Una causa común de confusión para los nuevos programadores de C++ es la
asignación. De esto no hay duda porque el signo =
es
una operación fundamental en la programación, directamente hasta copiar un
registro en el nivel de máquina. Además, el constructor de copia (descrito
en el capítulo 11) [FIXME:referencia] es invocado cuando el signo
=
se usa así:
MyType b; MyType a = b; a = b;
En la segunda línea, se define el objeto a
. Se crea un nuevo
objeto donde no existía ninguno. Dado que ya sabe hasta que punto es quisquilloso el
compilador de C++ respecto a la inicialización de objetos, sabrá que cuando se
define un objeto, siempre se invoca un constructor. Pero ¿qué constructor?,
a
se crea desde un objeto existente MyType
(b
, en el lado derecho del signo de igualdad), así que solo hay
una opción: el constructor de copia. Incluso aunque el signo de igualdad esté
involucrado, se llama al constructor de copia.
En la tercera línea, las cosas son diferentes. En la parte izquierda del signo
igual, hay un objeto previamente inicializado. Claramente, no se invoca un
constructor para un objeto que ya ha sido creado. En este caso
MyType::operator=
se llama para a
, tomando
como argumento lo que sea que aparezca en la parte derecha. (Puede tener varias
funciones operator=
que tomen diferentes argumentos en la parte
derecha).
Este comportamiento no está restringido al constructor de copia. Cada vez que
inicializa un objeto usando un signo =
en lugar de la forma usual
de llamada al constructor, el compilador buscará un constructor que acepte lo que
sea que haya en la parte derecha:
//: C12:CopyingVsInitialization.cpp class Fi { public: Fi() {} }; class Fee { public: Fee(int) {} Fee(const Fi&) {} }; int main() { Fee fee = 1; // Fee(int) Fi fi; Fee fum = fi; // Fee(Fi) } ///:~
Listado 12.13. C12/CopyingVsInitialization.cpp
Cuando se trata con el signo =
, es importante mantener la diferencia en
mente: Si el objeto no ha sido creado todavía, se requiere una inicialización; en
otro caso se usa el operador de asignación =
.
Es incluso mejor evitar escribir código que usa =
para la
inicialización; en cambio, use siempre la manera del constructor explícito. Las dos
construcciones con el signo igual se convierten en:
Fee fee(1); Fee fum(fi);
De esta manera, evitará confundir a sus lectores.
En Integer.h
y en Byte.h
vimos que el
operador =
sólo puede ser una función miembro. Está íntimamente
ligado al objeto que hay en la parte izquierda del =
. Si fuese
posible definir operator=
de forma global, entonces podría
intentar redefinir el signo =
del lenguaje:
int operator=(int, MyType); // Global = !No permitido!
El compilador evita esta situación obligandole a hacer un método
operator=
.
Cuando cree un operator=
, debe copiar toda la información necesaria desde
el objeto de la parte derecha al objeto actual (es decir, el objeto para el que
operator=
está siendo llamado) para realizar lo que sea que considere
«asignación» para su clase. Para objetos simples, esto es trivial:
//: C12:SimpleAssignment.cpp // Simple operator=() #include <iostream> using namespace std; class Value { int a, b; float c; public: Value(int aa = 0, int bb = 0, float cc = 0.0) : a(aa), b(bb), c(cc) {} Value& operator=(const Value& rv) { a = rv.a; b = rv.b; c = rv.c; return *this; } friend ostream& operator<<(ostream& os, const Value& rv) { return os << "a = " << rv.a << ", b = " << rv.b << ", c = " << rv.c; } }; int main() { Value a, b(1, 2, 3.3); cout << "a: " << a << endl; cout << "b: " << b << endl; a = b; cout << "a after assignment: " << a << endl; } ///:~
Listado 12.14. C12/SimpleAssignment.cpp
Aquí, el objeto de la parte izquierda del igual copia todos los elementos del objeto de la parte derecha, y entonces devuelve una referencia a sí mismo, lo que permite crear expresiones más complejas.
Este ejemplo incluye un error comón. Cuando asignane dos objetos del mismo tipo, siempre debería comprobar primero la auto-asignación: ¿Está asignado el objeto a sí mismo?. En algunos casos como éste, es inofensivo si realiza la operación de asignación en todo caso, pero si se realizan cambios a la implementación de la clase, puede ser importante y si no lo toma con una cuestión de costumbre, puede olvidarlo y provocar errores difíciles de encontrar.
¿Qué ocurre si el objeto no es tan simple?. Por ejemplo, ¿qué pasa si el objeto contiene punteros a otros objetos?. Sólo copiar el puntero significa que obtendrá dos objetos que apuntan a la misma localización de memoria. En situaciones como ésta, necesita hacer algo de contabilidad.
Hay dos aproximaciones a este problema. La técnica más simple es copiar lo que quiera que apunta el puntero cuando realiza una asignación o una construcción de copia. Esto es sencillo:
//: C12:CopyingWithPointers.cpp // Solving the pointer aliasing problem by // duplicating what is pointed to during // assignment and copy-construction. #include "../require.h" #include <string> #include <iostream> using namespace std; class Dog { string nm; public: Dog(const string& name) : nm(name) { cout << "Creating Dog: " << *this << endl; } // Synthesized copy-constructor & operator= // are correct. // Create a Dog from a Dog pointer: Dog(const Dog* dp, const string& msg) : nm(dp->nm + msg) { cout << "Copied dog " << *this << " from " << *dp << endl; } ~Dog() { cout << "Deleting Dog: " << *this << endl; } void rename(const string& newName) { nm = newName; cout << "Dog renamed to: " << *this << endl; } friend ostream& operator<<(ostream& os, const Dog& d) { return os << "[" << d.nm << "]"; } }; class DogHouse { Dog* p; string houseName; public: DogHouse(Dog* dog, const string& house) : p(dog), houseName(house) {} DogHouse(const DogHouse& dh) : p(new Dog(dh.p, " copy-constructed")), houseName(dh.houseName + " copy-constructed") {} DogHouse& operator=(const DogHouse& dh) { // Check for self-assignment: if(&dh != this) { p = new Dog(dh.p, " assigned"); houseName = dh.houseName + " assigned"; } return *this; } void renameHouse(const string& newName) { houseName = newName; } Dog* getDog() const { return p; } ~DogHouse() { delete p; } friend ostream& operator<<(ostream& os, const DogHouse& dh) { return os << "[" << dh.houseName << "] contains " << *dh.p; } }; int main() { DogHouse fidos(new Dog("Fido"), "FidoHouse"); cout << fidos << endl; DogHouse fidos2 = fidos; // Copy construction cout << fidos2 << endl; fidos2.getDog()->rename("Spot"); fidos2.renameHouse("SpotHouse"); cout << fidos2 << endl; fidos = fidos2; // Assignment cout << fidos << endl; fidos.getDog()->rename("Max"); fidos2.renameHouse("MaxHouse"); } ///:~
Listado 12.15. C12/CopyingWithPointers.cpp
Dog
es una clase simple que contiene solo una cadena con
el nombre del perro. Sin embargo, generalmente sabrá cuando le sucede algo al
perro porque los constructores y destructores imprimen información cuando se
invocan. Fíjese que el segundo constructor es un poco como un constructor de
copia excepto que toma un puntero a Dog
en vez de una
referencia, y tiene un segundo argumento que es un mensaje a ser concatenado con
el nombre del perro. Esto se hace así para ayudar a rastrear el comportamiento
del programa.
Puede ver que cuando un método imprime información, no accede a esa información
directamente sino que manda *this
a
cout
. Éste a su vez llama a ostream
operator<<
. Es aconsejable hacer esto así dado que si quiere
reformatear la manera en la que información del perro es mostrada (como hice
añadiendo el «[» y el «]») solo necesita hacerlo en un
lugar.
Una DogHouse
contiene un Dog*
y
demuestra las cuatro funciones que siempre necesitará definir cuando sus clases
contengan punteros: todos los constructores necesarios usuales, el constructor
de copia, operator=
(se define o se deshabilita) y un
destructor. Operator=
comprueba la auto-asignación como una
cuestión de estilo, incluso aunque no es estrictamente necesario aquí. Esto
virtualmente elimina la posibilidad de que olvide comprobar la auto-asignación
si cambia el código.
En el ejemplo de arriba, el constructor de copia y el operador =
realizan una copia de lo que apunta el puntero, y el destructor lo borra. Sin
embargo, si su objeto requiere una gran cantidad de memoria o una gran
inicialización fija, a lo mejor puede querer evitar esta copia. Una aproximación
común a este problema se llama conteo de referencias. Se le
da inteligencia al objeto que está siendo apuntado de tal forma que sabe cuántos
objetos le están apuntado. Entonces la construcción por copia o la asignación
consiste en añadir otro puntero a un objeto existente e incrementar la cuenta de
referencias. La destrucción consiste en reducir esta cuenta de referencias y
destruir el objeto si la cuenta llega a cero.
¿Pero que pasa si quiere escribir el objeto(Dog
en el ejemplo
anterior)?. Más de un objeto puede estar usando este Dog
luego podría estar modificando el perro de alguien más a la vez que el suyo, lo
cual no parece ser muy amigable. Para resolver este problema de
«solapamiento» se usa una técnica adicional llamada
copia-en-escritura. Antes de escribir un bloque de memoria,
debe asegurarse que nadie más lo está usando. Si la cuenta de referencia es
superior a uno, debe realizar una copia personal del bloque antes de escribirlo,
de tal manera que no moleste el espacio de otro. He aquí un ejemplo simple de
conteo de referencias y copia-en-escritura:
//: C12:ReferenceCounting.cpp // Reference count, copy-on-write #include "../require.h" #include <string> #include <iostream> using namespace std; class Dog { string nm; int refcount; Dog(const string& name) : nm(name), refcount(1) { cout << "Creating Dog: " << *this << endl; } // Prevent assignment: Dog& operator=(const Dog& rv); public: // Dogs can only be created on the heap: static Dog* make(const string& name) { return new Dog(name); } Dog(const Dog& d) : nm(d.nm + " copy"), refcount(1) { cout << "Dog copy-constructor: " << *this << endl; } ~Dog() { cout << "Deleting Dog: " << *this << endl; } void attach() { ++refcount; cout << "Attached Dog: " << *this << endl; } void detach() { require(refcount != 0); cout << "Detaching Dog: " << *this << endl; // Destroy object if no one is using it: if(--refcount == 0) delete this; } // Conditionally copy this Dog. // Call before modifying the Dog, assign // resulting pointer to your Dog*. Dog* unalias() { cout << "Unaliasing Dog: " << *this << endl; // Don't duplicate if not aliased: if(refcount == 1) return this; --refcount; // Use copy-constructor to duplicate: return new Dog(*this); } void rename(const string& newName) { nm = newName; cout << "Dog renamed to: " << *this << endl; } friend ostream& operator<<(ostream& os, const Dog& d) { return os << "[" << d.nm << "], rc = " << d.refcount; } }; class DogHouse { Dog* p; string houseName; public: DogHouse(Dog* dog, const string& house) : p(dog), houseName(house) { cout << "Created DogHouse: "<< *this << endl; } DogHouse(const DogHouse& dh) : p(dh.p), houseName("copy-constructed " + dh.houseName) { p->attach(); cout << "DogHouse copy-constructor: " << *this << endl; } DogHouse& operator=(const DogHouse& dh) { // Check for self-assignment: if(&dh != this) { houseName = dh.houseName + " assigned"; // Clean up what you're using first: p->detach(); p = dh.p; // Like copy-constructor p->attach(); } cout << "DogHouse operator= : " << *this << endl; return *this; } // Decrement refcount, conditionally destroy ~DogHouse() { cout << "DogHouse destructor: " << *this << endl; p->detach(); } void renameHouse(const string& newName) { houseName = newName; } void unalias() { p = p->unalias(); } // Copy-on-write. Anytime you modify the // contents of the pointer you must // first unalias it: void renameDog(const string& newName) { unalias(); p->rename(newName); } // ... or when you allow someone else access: Dog* getDog() { unalias(); return p; } friend ostream& operator<<(ostream& os, const DogHouse& dh) { return os << "[" << dh.houseName << "] contains " << *dh.p; } }; int main() { DogHouse fidos(Dog::make("Fido"), "FidoHouse"), spots(Dog::make("Spot"), "SpotHouse"); cout << "Entering copy-construction" << endl; DogHouse bobs(fidos); cout << "After copy-constructing bobs" << endl; cout << "fidos:" << fidos << endl; cout << "spots:" << spots << endl; cout << "bobs:" << bobs << endl; cout << "Entering spots = fidos" << endl; spots = fidos; cout << "After spots = fidos" << endl; cout << "spots:" << spots << endl; cout << "Entering self-assignment" << endl; bobs = bobs; cout << "After self-assignment" << endl; cout << "bobs:" << bobs << endl; // Comment out the following lines: cout << "Entering rename(\"Bob\")" << endl; bobs.getDog()->rename("Bob"); cout << "After rename(\"Bob\")" << endl; } ///:~
Listado 12.16. C12/ReferenceCounting.cpp
La clase Dog
es el objeto apuntado por
DogHouse
. Contiene una cuenta de referencias y métodos
para controlar y leer la cuenta de referencias. Hay un constructor de copia de
modo que puede crear un nuevo Dog
a partir de uno
existente.
La función attach()
incrementa la cuenta de referencias de
un Dog
para indicar que hay otro objeto usándolo. La
función detach()
decrementa la cuenta de referencias. Si
llega a cero, entonces nadie mas lo está usando, así que el método destruye su
propio objeto llamando a delete this
.
Antes de que haga cualquier modificación (como renombrar un
Dog
), debería asegurarse de que no está cambiando un
Dog
que algún otro objeto está usando. Hágalo llamando
a DogHouse::unalias()
, que llama a
Dog::unalias()
. Esta última función devolverá el puntero a
Dog
existente si la cuenta de referencias es uno (lo que
significa que nadie mas está usando ese Dog
), pero
duplicará Dog
si esa cuenta es mayor que uno.
El constructor de copia, además de pedir su propia memoria, asigna
Dog
al Dog
del objeto
fuente. Entonces, dado que ahora hay un objeto más usando ese bloque de memoria,
incrementa la cuenta de referencias llamando a Dog::attach()
.
El operador =
trata con un objeto que ha sido creado en la parte
izquierda del =
, así que primero debe limpiarlo llamando a
detach()
para ese Dog
, lo que
destruirá el Dog
viejo si nadie más lo está
usando. Entonces operator=
repite el comportamiento del constructor
de copia. Advierta que primero realiza comprobaciones para detectar cuando está
asignando el objeto a sí mismo.
El destructor llama a detach()
para destruir
condicionalmente el Dog
.
Para implementar la copia-en-escritura, debe controlar todas las operaciones que
escriben en su bloque de memoria. Por ejemplo, el método
renameDog()
le permite cambiar valores en el bloque de
memoria. Pero primero, llama a unalias()
para evitar la
modificación de un Dog
solapado (un
Dog
con más de un objeto DogHouse
apuntándole). Y si necesita crear un puntero a un Dog
desde un DogHouse
debe llamar primero a
unalias()
para ese puntero.
La función main()
comprueba varias funciones que
deben funcionar correctamente para implementar la cuenta de
referencias: el constructor, el constructor de copia,
operator=
y el destructor. También comprueba la
copia-en-escritura llamando a renameDog()
.
He aquí la salida (después de un poco de reformateo):
Creando Dog: [Fido], rc = 1 CreadoDogHouse: [FidoHouse] contiene [Fido], rc = 1 Creando Dog: [Spot], rc = 1 CreadoDogHouse: [SpotHouse] contiene [Spot], rc = 1 Entrando en el constructor de copia Dog añadido:[Fido], rc = 2 DogHouse constructor de copia [construido por copia FidoHouse] contiene [Fido], rc = 2 Despues de la construcción por copia de Bobs fidos:[FidoHouse] contiene [Fido], rc = 2 spots:[SpotHouse] contiene [Spot], rc = 1 bobs:[construido por copia FidoHouse] contiene[Fido], rc = 2 Entrando spots = fidos Eliminando perro: [Spot], rc = 1 Borrando Perro: [Spot], rc = 0 Añadido Dog: [Fido], rc = 3 DogHouse operador= : [FidoHouse asignado] contiene[Fido], rc = 3 Despues de spots = fidos spots:[FidoHouse asignado] contiene [Fido], rc = 3 Entrando en la auto asignación DogHouse operador= : [construido por copia FidoHouse] contiene [Fido], rc = 3 Despues de la auto asignación bobs:[construido por copia FidoHouse] contiene [Fido], rc = 3 Entando rename("Bob") Despues de rename("Bob") DogHouse destructor: [construido por copia FidoHouse] contiene [Fido], rc = 3 Eliminando perro: [Fido], rc = 3 DogHouse destructor: [FidoHouse asignado] contiene [Fido], rc = 2 Eliminando perro: [Fido], rc = 2 DogHouse destructor: [FidoHouse] contiene [Fido], rc = 1 Eliminando perro: [Fido], rc = 1 Borrando perro: [Fido], rc = 0
Estudiando la salida, rastreando el código fuente y experimentando con el programa, podrá ahondar en la comprensión de estas técnicas.
Dado que asignar un objeto a otro del mismo tipo es una
operación que la mayoría de la gente espera que sea posible, el compilador
automáticamente creará un type::operator=(type)
si usted el
programador no proporciona uno. El comportamiento de este operador imita el del
constructor de copia creado automáticamente; si la clase contiene objetos (o se
deriva de otra clase), se llama recursivamente a operator=
para
esos objetos. A esto se le llama asignación miembro a
miembro. Por ejemplo:
//: C12:AutomaticOperatorEquals.cpp #include <iostream> using namespace std; class Cargo { public: Cargo& operator=(const Cargo&) { cout << "inside Cargo::operator=()" << endl; return *this; } }; class Truck { Cargo b; }; int main() { Truck a, b; a = b; // Prints: "inside Cargo::operator=()" } ///:~
Listado 12.17. C12/AutomaticOperatorEquals.cpp
El operador=
generado automáticamente para
Truck
llama a Cargo::operator=
.
En general, no querrá que el compilador haga esto por usted. Con clases de
cualquier sofisticación (¡Especialmente si contienen punteros!) querrá crear de
forma explicita un operator=
. Si realmente no quiere que la
gente realice asignaciones, declare operator=
como una
método privado. (No necesita definirla a menos que la esté usando dentro de la
clase).