Copiar constructor y operador = sobrecarga en C ++: ¿es posible una función común?

87

Desde un constructor de copias

MyClass(const MyClass&);

y una = sobrecarga del operador

MyClass& operator = (const MyClass&);

tienen prácticamente el mismo código, el mismo parámetro y solo difieren en la devolución, ¿es posible tener una función común para que ambos la usen?

MPelletier
fuente
6
"... tiene prácticamente el mismo código ..."? Hmm ... Debes estar haciendo algo mal. Intente minimizar la necesidad de utilizar funciones definidas por el usuario para esto y deje que el compilador haga todo el trabajo sucio. Esto a menudo significa encapsular recursos en su propio objeto miembro. Podrías mostrarnos algún código. Quizás tengamos algunas buenas sugerencias de diseño.
Sellibitze
2
Posible duplicado de Reducción
mpromonet

Respuestas:

121

Si. Hay dos opciones comunes. Uno, que generalmente no se recomienda, es llamar operator=explícitamente desde el constructor de copia:

MyClass(const MyClass& other)
{
    operator=(other);
}

Sin embargo, proporcionar un bien operator=es un desafío cuando se trata de lidiar con el estado anterior y los problemas que surgen de la autoasignación. Además, todos los miembros y bases se inicializan por defecto primero, incluso si se les va a asignar desde other. Esto puede que ni siquiera sea válido para todos los miembros y bases e incluso cuando es válido es semánticamente redundante y puede resultar prácticamente caro.

Una solución cada vez más popular es implementar operator=utilizando el constructor de copia y un método de intercambio.

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

o incluso:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

Una swapfunción suele ser sencilla de escribir, ya que simplemente intercambia la propiedad de los componentes internos y no tiene que limpiar el estado existente ni asignar nuevos recursos.

Las ventajas del lenguaje de copiar e intercambiar es que es automáticamente seguro para la autoasignación y, siempre que la operación de intercambio sea sin tirar, también es muy seguro para excepciones.

Para estar fuertemente seguro de excepciones, un operador de asignación escrita 'a mano' generalmente tiene que asignar una copia de los nuevos recursos antes de desasignar los recursos antiguos del cesionario, de modo que si ocurre una excepción al asignar los nuevos recursos, el estado anterior aún se puede volver a . Todo esto viene gratis con copiar e intercambiar, pero generalmente es más complejo y, por lo tanto, propenso a errores, para hacerlo desde cero.

Lo único que debe tener cuidado es asegurarse de que el método de intercambio sea un intercambio verdadero, y no el predeterminado std::swapque usa el constructor de copia y el operador de asignación en sí.

Normalmente swapse utiliza un miembro . std::swapfunciona y está garantizado 'no-throw' con todos los tipos básicos y tipos de puntero. La mayoría de los punteros inteligentes también se pueden intercambiar con una garantía de no tirar.

CB Bailey
fuente
3
En realidad, no son operaciones habituales. Mientras que el copy ctor inicializa por primera vez los miembros del objeto, el operador de asignación anula los valores existentes. Teniendo esto en cuenta, operator=la copia del ctor es de hecho bastante mala, porque primero inicializa todos los valores a algunos valores predeterminados solo para anularlos con los valores del otro objeto justo después.
sbi
14
Tal vez a "No recomiendo", agregue "y ningún experto en C ++". Alguien puede llegar y no darse cuenta de que no solo estás expresando una preferencia personal de una minoría, sino la opinión consensuada de quienes realmente lo han pensado. Y, de acuerdo, tal vez me equivoque y algún experto en C ++ lo recomiende, pero personalmente, todavía dejaría el guante para que alguien presente una referencia para esa recomendación.
Steve Jessop
4
De todos modos, ya te he votado a favor :-). Me imagino que si algo se considera una buena práctica, entonces es mejor decirlo (y mirarlo de nuevo si alguien dice que no es realmente lo mejor después de todo). Del mismo modo, si alguien pregunta "¿es posible usar mutexes en C ++", yo no diría "una opción bastante común es ignorar completamente RAII y escribir código que no sea seguro para excepciones que se bloquea en producción, pero es cada vez más popular escribir código decente y funcional ";-)
Steve Jessop
4
+1. Y creo que siempre se necesita un análisis. Creo que es razonable tener una assignfunción miembro utilizada tanto por el ctor de copia como por el operador de asignación en algunos casos (para clases ligeras). En otros casos (casos de uso intensivo de recursos / manejo / cuerpo) una copia / intercambio es el camino a seguir, por supuesto.
Johannes Schaub - litb
2
@litb: Me sorprendió esto, así que busqué el artículo 41 en la Excepción C ++ (en la que se convirtió este mensaje) y esta recomendación en particular se ha ido y recomienda copiar e intercambiar en su lugar. Más bien a escondidas ha dejado caer el "Problema n. ° 4: es ineficiente para la asignación" al mismo tiempo.
CB Bailey
13

El constructor de copia realiza la inicialización por primera vez de objetos que solían ser memoria sin formato. El operador de asignación, OTOH, reemplaza los valores existentes por otros nuevos. La mayoría de las veces, esto implica descartar recursos antiguos (por ejemplo, memoria) y asignar nuevos.

Si hay una similitud entre los dos, es que el operador de asignación realiza la destrucción y la construcción de copias. Algunos desarrolladores solían implementar la asignación mediante destrucción in situ seguida de construcción de copia de ubicación. Sin embargo, esta es una muy mala idea. (¿Qué pasa si este es el operador de asignación de una clase base que llamó durante la asignación de una clase derivada?)

Lo que generalmente se considera el idioma canónico hoy en día es usar swapcomo sugirió Charles:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

Esto usa construcción de copia (tenga en cuenta que otherse copia) y destrucción (se destruye al final de la función), y también los usa en el orden correcto: construcción (puede fallar) antes de la destrucción (no debe fallar).

sbi
fuente
¿Debería swapdeclararse virtual?
1
@Johannes: Las funciones virtuales se utilizan en jerarquías de clases polimórficas. Los operadores de asignación se utilizan para tipos de valor. Los dos apenas se mezclan.
sbi
-3

Algo me molesta por:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

Primero, leer la palabra "intercambio" cuando mi mente está pensando en "copiar" irrita mi sentido común. Además, cuestiono el objetivo de este elegante truco. Sí, cualquier excepción en la construcción de los nuevos recursos (copiados) debe ocurrir antes del intercambio, lo que parece una forma segura de asegurarse de que se completen todos los datos nuevos antes de ponerlos en funcionamiento.

Esta bien. Entonces, ¿qué pasa con las excepciones que ocurren después del intercambio? (cuando los recursos antiguos se destruyen cuando el objeto temporal sale del alcance) Desde la perspectiva del usuario de la asignación, la operación ha fallado, excepto que no lo hizo. Tiene un efecto secundario enorme: la copia realmente sucedió. Fue solo una limpieza de recursos la que falló. El estado del objeto de destino se ha modificado aunque la operación parece haber fallado desde el exterior.

Entonces, propongo en lugar de "swap" hacer una "transferencia" más natural:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    transfer(tmp);
    return *this;
}

Todavía existe la construcción del objeto temporal, pero la siguiente acción inmediata es liberar todos los recursos actuales del destino antes de mover (y anular para que no se liberen dos veces) los recursos de la fuente hacia él.

En lugar de {construir, mover, destruir}, propongo {construir, destruir, mover}. El movimiento, que es la acción más peligrosa, es el último que se toma después de que todo lo demás se ha resuelto.

Sí, la destrucción fallida es un problema en cualquier esquema. Los datos están dañados (copiados cuando no pensaba que lo estaban) o perdidos (liberados cuando no pensaba que lo estaban). Perdido es mejor que corrompido. Ningún dato es mejor que un dato incorrecto.

Transferir en lugar de intercambiar. De todos modos, esa es mi sugerencia.

Mateo
fuente
2
Un destructor no debe fallar, por lo que no se esperan excepciones tras la destrucción. Y no entiendo cuál sería la ventaja de mover el movimiento detrás de la destrucción, si el movimiento es la operación más peligrosa. Es decir, en el esquema estándar, una falla de movimiento no corromperá el estado anterior, mientras que su esquema nuevo lo hará. ¿Entonces por qué? Además, First, reading the word "swap" when my mind is thinking "copy" irritates-> Como escritor de una biblioteca, normalmente conoce las prácticas habituales (copiar + intercambiar), y el quid de la cuestión es my mind. Tu mente está realmente oculta detrás de la interfaz pública. De eso se trata el código reutilizable.
Sebastian Mach