11.3.2. Construcción por copia

El problema se produce debido a que el compilador hace una suposición sobre cómo crear un nuevo objeto a partir de otro existente. Cuando se pasa un objeto por valor, se crea un nuevo objeto, que estará dentro del marco de la función, a partir del objeto original existente fuera del marco de la función. Esto también se puede aplicar a menudo cuando una función retorna un objeto. En la expresión

HowMany h2 = f(h);

h2, un objeto que no estaba creado anteriormente, se crea a partir del valor que retorna f(); por tanto también se crea un nuevo objeto a partir de otro existente.

El compilador supone que la creación ha de hacerse con una copia bit a bit, lo que en muchos casos funciona bien, pero en HowMany no funciona porque la inicialización va más allá de una simple copia. Otro ejemplo muy común ocurre cuando la clase contiene punteros pues, ¿a qué deben apuntar? ¿Debería copiar sólo los punteros o debería asignar memoria nueva y que apuntaran a ella?

Afortunadamente, puede intervenir en este proceso e impedir que el compilador haga una copia bit a bit. Se soluciona definiendo su propia función, que se utilizará siempre que el compilador necesite crear un nuevo objeto a partir de otro. Lógicamente, está creando un nuevo objeto, por lo que esta función es un constructor, y el único argumento del constructor tiene que ver con el objeto del que se pretende partir para crear el nuevo. Pero no puede pasar ese objeto por valor al constructor porque está intentando definir la función que maneja el paso por valor, y, por otro lado, sintácticamente no tiene sentido pasar un puntero porque, después de todo, está creando un objeto a partir de otro. Aquí es cuando las referencias vienen al rescate, y puede utilizar la referencia del objeto origen. Esta función se llama constructor de copia, y a menudo se la denomina X(X&), que es su aspecto para una clase llamada X.

Si crea un constructor de copia, el compilador no realizará una copia bit a bit cuando cree un nuevo objeto a partir de otro. El compilador siempre llamará al constructor de copia. Si no crea el constructor de copia, el compilador intentará hacer algo razonable, pero usted tiene la opción de tener control total del proceso.

Ahora es posible solucionar el problema en HowMany.cpp:

//: C11:HowMany2.cpp
// The copy-constructor
#include <fstream>
#include <string>
using namespace std;
ofstream out("HowMany2.out");

class HowMany2 {
  string name; // Object identifier
  static int objectCount;
public:
  HowMany2(const string& id = "") : name(id) {
    ++objectCount;
    print("HowMany2()");
  }
  ~HowMany2() {
    --objectCount;
    print("~HowMany2()");
  }
  // The copy-constructor:
  HowMany2(const HowMany2& h) : name(h.name) {
    name += " copy";
    ++objectCount;
    print("HowMany2(const HowMany2&)");
  }
  void print(const string& msg = "") const {
    if(msg.size() != 0) 
      out << msg << endl;
    out << '\t' << name << ": "
        << "objectCount = "
        << objectCount << endl;
  }
};

int HowMany2::objectCount = 0;

// Pass and return BY VALUE:
HowMany2 f(HowMany2 x) {
  x.print("x argument inside f()");
  out << "Returning from f()" << endl;
  return x;
}

int main() {
  HowMany2 h("h");
  out << "Entering f()" << endl;
  HowMany2 h2 = f(h);
  h2.print("h2 after call to f()");
  out << "Call f(), no return value" << endl;
  f(h);
  out << "After call to f()" << endl;
} ///:~

Listado 11.7. C11/HowMany2.cpp


Hay unas cuantas cosas nuevas para que pueda hacerse una idea mejor de lo que pasa. Primeramente, el string name hace de identificador de objeto cuando se imprime información sobre él. Puede poner un identificador (normalmente el nombre del objeto) en el constructor para que se copie en name utilizando el constructor con un string como argumento. Por defecto se crea un string vacío. El constructor incrementa objectCount y el destructor lo disminuye, igual que en el ejemplo anterior.

Lo siguiente es el constructor de copia, HowMany2(const HowMany2&). El constructor de copia sólo puede crear un objeto a partir de otro existente, así que copia en name el identificador del objeto origen, seguido de la palabra «copy», y así puede ver de dónde procede. Si mira atentamente, verá que la llamada name(h.name) en la lista de inicialización del constructor está llamando al constructor de copia de la clase string.

Dentro del constructor de copia, se incrementa el contador igual que en el constructor normal. Esto quiere decir que obtendrá un contador de objetos preciso cuando pase y retorne por valor.

La función print() se ha modificado para imprimir en la salida un mensaje, el identificador del objeto y el contador de objetos. Como ahora accede al atributo name de un objeto concreto, ya no puede ser un método estático.

Dentro de main() puede ver que hay una segunda llamada a f(). Sin embargo esta llamada utiliza el enfoque habitual en C de ignorar el valor de retorno. Pero ahora que sabe cómo se retorna el valor (es decir, el código dentro de la función maneja el proceso de retorno poniendo el resultado en un lugar cuya dirección se pasa como un argumento escondido), podría preguntarse qué ocurre cuando se ignora el valor de retorno. La salida del programa arrojará algo de luz sobre el asunto.

Pero antes de mostrar la salida, he aquí un pequeño programa que utiliza iostreams para añadir números de línea a cualquier fichero:

//: C11:Linenum.cpp
//{T} Linenum.cpp
// Add line numbers
#include "../require.h"
#include <vector>
#include <string>
#include <fstream>
#include <iostream>
#include <cmath>
using namespace std;

int main(int argc, char* argv[]) {
  requireArgs(argc, 1, "Usage: linenum file\n"
    "Adds line numbers to file");
  ifstream in(argv[1]);
  assure(in, argv[1]);
  string line;
  vector<string> lines;
  while(getline(in, line)) // Read in entire file
    lines.push_back(line);
  if(lines.size() == 0) return 0;
  int num = 0;
  // Number of lines in file determines width:
  const int width = 
    int(log10((double)lines.size())) + 1;
  for(int i = 0; i < lines.size(); i++) {
    cout.setf(ios::right, ios::adjustfield);
    cout.width(width);
    cout << ++num << ") " << lines[i] << endl;
  }
} ///:~

Listado 11.8. C11/Linenum.cpp


El fichero se pasa a un vector<string>, utilizando el mismo código fuente que ha visto anteriormente en este libro. Cuando se ponen los números de línea, conviene que todas las líneas estén alineadas, y para ello hay que tener en cuenta el número de líneas del fichero de modo que el ancho reservado para los números de línea sea siempre el mismo. Se puede conocer el número de líneas con vector::size(), pero lo que realmente hace falta saber es si hay más de 10 líneas, 100, 1000, etc. Si se utiliza el logaritmo en base 10 sobre el número de líneas en el fichero, se trunca a un entero y se añade uno al valor resultante, eso determinará el ancho máximo en dígitos que un número de línea puede tener.

Nótese que hay un par de llamadas extrañas dentro del bucle for: setf() y width(). Son llamadas de ostream que permiten controlar, en este caso, la justificación y anchura de la salida. Sin embargo se deben llamar cada vez que se imprime una línea y por eso están dentro del bucle for. El Volumen 2 de este libro tiene un capítulo entero que explica los iostreams y que cuenta más sobre estas llamadas así como otras formas de controlar los iostreams.

Cuando se aplica Linenum.cpp a HowMany2.out, resulta:

1) HowMany2()
2)   h: objectCount = 1
3) Entering f()
4) HowMany2(const HowMany2&)
5)   h copy: objectCount = 2
6) x argument inside f()
7)   h copy: objectCount = 2
8) Returning from f()
9) HowMany2(const HowMany2&)
10)   h copy copy: objectCount = 3
11) ~HowMany2()
12)   h copy: objectCount = 2
13) h2 after call to f()
14)   h copy copy: objectCount = 2
15) Call f(), no return value
16) HowMany2(const HowMany2&)
17)   h copy: objectCount = 3
18) x argument inside f()
19)   h copy: objectCount = 3
20) Returning from f()
21) HowMany2(const HowMany2&)
22)   h copy copy: objectCount = 4
23) ~HowMany2()
24)   h copy: objectCount = 3
25) ~HowMany2()
26)   h copy copy: objectCount = 2
27) After call to f()
28) ~HowMany2()
29)   h copy copy: objectCount = 1
30) ~HowMany2()
31)   h: objectCount = 0

Como cabía esperar, la primera cosa que ocurre es que para h se llama al constructor normal, el cual incrementa el contador de objetos a uno. Pero entonces, mientras se entra en f(), el compilador llama silenciosamente al constructor de copia para hacer el paso por valor. Se crea un nuevo objeto, que es copia de h (y por tanto tendrá el identificador «h copy») dentro del marco de la función f(). Así pues, el contador de objetos se incrementa a dos, por cortesía del constructor de copia.

La línea ocho indica el principio del retorno de f(). Pero antes de que se destruya la variable local «h copy» (pues sale de ámbito al final de la función), se debe copiar al valor de retorno, que es h2. Por tanto h2, que no estaba creado previamente, se crea a partir de un objeto ya existente (la variable local dentro de f()) y el constructor de copia vuelve a utilizarse en la línea 9. Ahora el identificador de h2 es «h copy copy» porque copió el identificador de la variable local de f(). Después de devolver el objeto, pero antes de que la función termine, el contador de objetos se incrementa temporalmente a tres, pero la variable local con identificador «h copy» se destruye, disminuyendo a dos. Después de que se complete la llamada a f() en la línea 13, sólo hay dos objetos, h y h2, y puede comprobar, de hecho, que h2 terminó con el identificador «h copy copy».

Objetos temporales

En la línea 15 se empieza la llamada a f(h), y esta vez se ignora el valor de retorno. Puede ver que se invoca el constructor de copia en la línea 16, igual que antes, para pasar el argumento. Y también, igual que antes, en la línea 21 se llama al constructor de copia para el valor de retorno. Pero el constructor de copia necesita una dirección para utilizar como destino (es decir, para trabajar con el puntero this). ¿De dónde procede esta dirección?

Resulta que el compilador puede crear un objeto temporal cuando lo necesita para evaluar adecuadamente una expresión. En este caso, crea uno, que ni siquiera se ve, para que actúe como destino del valor de retorno ignorado de f(). El tiempo de vida de este objeto temporal es tan corto como sea posible para que el programa no se llene de objetos temporales esperando a ser destruidos, lo cual provocaría la utilización ineficiente de recursos valiosos. En algunos casos, el objeto temporal podría pasarse inmediatamente a otra función, pero en este caso no se necesita después de la llamada a la función, así que en cuanto la función termina, llamando al destructor del objeto local (líneas 23 y 24), el objeto temporal también se destruye (líneas 25 y 26).

Finalmente, de la línea 28 a la línea 31, se destruye el objeto h2, seguido de h y el contador de objetos vuelve correctamente a cero.