16.6. Manejando objetos por valor

Actualmente crear una copia de los objetos dentro de un contenedor genérico sería un problema complejo si no se tuvieran plantillas. Con los templates las cosas se vuelven relativamente sencillas - sólo hay que indicar que se están manejando objetos en vez de punteros:

//: C16:ValueStack.h
// Holding objects by value in a Stack
#ifndef VALUESTACK_H
#define VALUESTACK_H
#include "../require.h"

template<class T, int ssize = 100>
class Stack {
  // Default constructor performs object
  // initialization for each element in array:
  T stack[ssize];
  int top;
public:
  Stack() : top(0) {}
  // Copy-constructor copies object into array:
  void push(const T& x) {
    require(top < ssize, "Too many push()es");
    stack[top++] = x;
  }
  T peek() const { return stack[top]; }
  // Object still exists when you pop it; 
  // it just isn't available anymore:
  T pop() {
    require(top > 0, "Too many pop()s");
    return stack[--top];
  }
};
#endif // VALUESTACK_H ///:~

Listado 16.17. C16/ValueStack.h


El constructor de copia de los objetos contenidos hacen la mayoría del trabajo pasando y devolviendo objetos por valor. Dentro de push(), el almacenamiento del objeto en el array Stack viene acompañado con T::operator=. Para garantizar que funciona, una clase llamada SelfCounter mantiene una lista de las creaciones y construcciones de copia de los objetos.

//: C16:SelfCounter.h
#ifndef SELFCOUNTER_H
#define SELFCOUNTER_H
#include "ValueStack.h"
#include <iostream>

class SelfCounter {
  static int counter;
  int id;
public:
  SelfCounter() : id(counter++) {
    std::cout << "Created: " << id << std::endl;
  }
  SelfCounter(const SelfCounter& rv) : id(rv.id){
    std::cout << "Copied: " << id << std::endl;
  }
  SelfCounter operator=(const SelfCounter& rv) {
    std::cout << "Assigned " << rv.id << " to " 
              << id << std::endl;
    return *this;
  }
  ~SelfCounter() {
    std::cout << "Destroyed: "<< id << std::endl;
  }
  friend std::ostream& operator<<( 
    std::ostream& os, const SelfCounter& sc){
    return os << "SelfCounter: " << sc.id;
  }
};
#endif // SELFCOUNTER_H ///:~

Listado 16.18. C16/SelfCounter.h


//: C16:SelfCounter.cpp {O}
#include "SelfCounter.h"
int SelfCounter::counter = 0; ///:~

Listado 16.19. C16/SelfCounter.cpp


//: C16:ValueStackTest.cpp
//{L} SelfCounter
#include "ValueStack.h"
#include "SelfCounter.h"
#include <iostream>
using namespace std;

int main() {
  Stack<SelfCounter> sc;
  for(int i = 0; i < 10; i++)
    sc.push(SelfCounter());
  // OK to peek(), result is a temporary:
  cout << sc.peek() << endl;
  for(int k = 0; k < 10; k++)
    cout << sc.pop() << endl;
} ///:~

Listado 16.20. C16/ValueStackTest.cpp


Cuando se crea un contenedor Stack, el constructor por defecto del objeto a contener es ejecutado por cada objeto en el array. Inicialmente se verán 100 objetos SelfCounter creados sin ningún motivo aparente, pero esto es justamente la inicialización del array. Esto puede resultar un poco caro, pero no existe ningún problema en un diseño simple como este. Incluso en situaciones más complejas si se hace a Stack más general permitiendo que crezca dinámicamente, porque en la implementación mostrada anteriormente esto implicaría crear un nuevo array más grande, copiando el anterior al nuevo y destruyendo el antiguo array (de hecho, así es como lo hace la clase vector de la Librería Estándar de C++).