47

En C++, el concepto de devolver la referencia desde el operador de asignación de copia no está claro para mí. ¿Por qué el operador de asignación de copias no puede devolver una copia del nuevo objeto? Además, si tengo clase A, y los siguientes:¿Por qué el operador de asignación de copia debe devolver una referencia/referencia constante?

A a1(param); 
A a2 = a1; 
A a3; 

a3 = a2; //<--- this is the problematic line 

El operador = se define como sigue:

A A::operator =(const A& a) 
{ 
    if (this == &a) 
    { 
     return *this; 
    } 
    param = a.param; 
    return *this; 
} 
+3

no hay tal requisito. Pero si quieres apegarte al principio de la menor sorpresa, devolverás 'A &' como 'a = b' es una expresión lvalue que hace referencia a' a' en caso de que 'a' y' b' sean enteros. – sellibitze

+0

@MattMcNabb ¡Gracias por avisarme! Hará eso – bks

+0

¿Por qué no podemos devolver 'A *' del operador de asignación de copias Supongo que la asignación de encadenamiento aún funcionaría correctamente. ¿Alguien puede ayudar a comprender los peligros de devolver 'A *' si hay alguno. –

Respuesta

54

Estrictamente hablando, el resultado de un operador de asignación de copia no necesita devolver una referencia, aunque para imitar el comportamiento predeterminado que utiliza el compilador de C++, debe devolver una referencia no const al objeto que se le asigna (un operador de asignación de copias generado implícitamente devolverá una referencia no const - C++ 03: 12.8/10). He visto un poco de código que devuelve void de sobrecargas de asignación de copias, y no puedo recordar cuándo eso causó un problema grave. Devolver void impedirá a los usuarios "encadenamiento de asignación" (a = b = c;), y evitará el uso del resultado de una asignación en una expresión de prueba, por ejemplo. Si bien ese tipo de código no es algo inaudito, tampoco creo que sea particularmente común, especialmente para los tipos no primitivos (a menos que la interfaz de una clase se proponga para este tipo de pruebas, como para iostremas).

No recomiendo que haga esto, solo indicando que está permitido y que no parece causar muchos problemas.

Estas otras preguntas SO están relacionadas (probablemente no del todo engaños) que tienen información/opiniones que pueden ser de su interés.

+0

Me pareció útil anular el vacío en el operador de asignación cuando necesitaba evitar la destrucción automática de objetos como vinieron de la pila . Para los objetos contados por ref, no quiere que se invoquen destructores cuando no los conoce. – cjcurrie

+0

+1 una de las dos únicas respuestas correctas. –

4

Es en parte porque el retorno de una referencia a sí mismo es más rápido que volver por valor, pero además, es para permitir la semántica original que existe en los tipos primitivos.

+0

No va a votar, pero me gustaría señalar que regresar por valor no tendría sentido. Imagine (a = b = c) si (a = b) devolvió 'a' por valor. Tu último punto es muy legítimo. – stinky472

+4

Obtendrá (a = (b = c)), creo, lo que aún produciría el resultado deseado. Solo si lo hiciste (a = b) = c se rompería. – Puppy

4

operator= puede definirse para devolver lo que desee. Debe ser más específico en cuanto a cuál es el problema en realidad; Sospecho que tiene el constructor de copia que usa operator= internamente y que causa un desbordamiento de pila, ya que el constructor de copia llama al operator=, que debe usar el constructor de copia para devolver A por valor ad infinitum.

+0

Eso sería una implementación coartada (e inusual) copy-ctor. La razón para devolver 'A &' de 'A :: operator =' es diferente en la mayoría de los casos. – jpalecek

+0

@jpalecek, estoy de acuerdo, pero teniendo en cuenta la publicación original y la falta de claridad al establecer el problema real, lo más probable es que la ejecución del operador de asignación produzca un stackoverflow debido a la recursión infinita. Si hay otra explicación para esta pregunta, me encantaría saberlo. – MSN

+0

@MSN No sé si fue su problema o no. Pero seguramente su publicación aquí se ha dirigido a mi problema +1 para que – Invictus

43

Un poco de aclaración en cuanto a por qué es preferible volver por referencia para operador = frente retorno por valor --- como la cadena a = b = c funcionará bien si se devuelve un valor.

Si devuelve una referencia, se realiza un trabajo mínimo. Los valores de un objeto se copian en otro objeto.

Sin embargo, si vuelve por valor de operador =, se le llame a un constructor y el destructor cada vez que el operador de asignación se llama !!

Así que dado:

A& operator=(constA& rhs){ ... }; 

Entonces

a=b=c;// calls assignment operator above twice. Nice and simple. 

pero

A operator=(constA& rhs){ ... }; 

a=b=c; // calls assignment operator twice, calls copy constructor twice, calls destructor type to delete the temporary values! Very wasteful and nothing gained! 

En suma, no hay nada ganado mediante la devolución por valor, pero mucho que perder.

(Nota: Esto no se pretende dar respuesta a las ventajas de tener el operador de asignación devolver un valor L Leer los otros puestos para qué puede ser preferible.)

+0

Muy buena respuesta. Ojalá pudiera dar +10 punto a esta respuesta. Excelente. Seguid así !!! – Destructor

8

Cuando se sobrecarga operator=, que puede escríbalo para devolver el tipo que desee. Si lo desea lo suficiente, puede sobrecargar X::operator= para devolver (por ejemplo) una instancia de alguna clase completamente diferente Y o Z. Esto generalmente es altamente desaconsejable.

En particular, generalmente desea admitir el encadenamiento de operator= como C lo hace. Por ejemplo:

int x, y, z; 

x = y = z = 0; 

Siendo ese el caso, normalmente se desea devolver un valor izquierdo o rvalue del tipo de ser asignado a. Eso solo deja la cuestión de si devolver una referencia a X, una referencia constante a X o una X (por valor).

Devolver una referencia constante a X es generalmente una mala idea. En particular, se permite que una referencia constante se una a un objeto temporal. El tiempo de vida del temporal se extiende a la duración de la referencia a la que está vinculado, pero no recursivamente a la duración de lo que pueda asignarse. Esto hace que sea más fácil devolver una referencia colgante: la referencia constante se vincula a un objeto temporal. La duración de ese objeto se extiende a la duración de la referencia (que finaliza al final de la función). En el momento en que la función retorna, la vida útil de la referencia y temporal han terminado, entonces lo que se asigna es una referencia colgante.

Por supuesto, devolver una referencia no const no proporciona una protección completa contra esto, pero al menos te hace trabajar un poco más duro. Todavía puede (por ejemplo) definir algún local y devolverle una referencia (pero la mayoría de los compiladores pueden advertir sobre esto también).

Devolver un valor en lugar de una referencia tiene problemas teóricos y prácticos. En el aspecto teórico, normalmente tiene una desconexión básica entre = y lo que significa en este caso. En particular, cuando la asignación normalmente significa "tomar esta fuente existente y asignar su valor a este destino existente", comienza a significar algo más parecido a "tomar esta fuente existente, crear una copia y asignar ese valor a este destino existente". "

Desde un punto de vista práctico, especialmente antes de que se inventaran las referencias rvalue, eso podría tener un impacto significativo en el rendimiento: la creación de un objeto completamente nuevo en el proceso de copia A a B era inesperado y a menudo bastante lento. Si, por ejemplo, tuviera un vector pequeño y se lo asignara a un vector más grande, esperaría que, como máximo, tome el tiempo para copiar los elementos del pequeño vector más una (pequeña) sobrecarga fija para ajustar el tamaño de el vector de destino.Si eso involucrara dos copias, una desde la fuente a la temperatura, otra desde la temperatura hasta el destino, y (peor) una asignación dinámica para el vector temporal, mi expectativa sobre la complejidad de la operación sería completamente destruida. Para un vector pequeño, el tiempo para la asignación dinámica podría ser muchas veces mayor que el tiempo para copiar los elementos.

La única otra opción (agregada en C++ 11) sería devolver una referencia rvalue. Esto podría conducir fácilmente a resultados inesperados: una asignación encadenada como a=b=c; podría destruir el contenido de b y/o c, lo que sería bastante inesperado.

Eso deja devolver una referencia normal (no una referencia a const, ni a una referencia de valor) como la única opción que (razonablemente) produce lo que la mayoría de la gente normalmente quiere.

+0

** + 1 ** No estoy seguro de la discusión de referencia pendiente, pero +1 para "usted puede". –

+0

No veo a qué situación de peligro se refiere en la parte "Devolución de una referencia de trabajo"; si alguien escribe 'const T & ref = T {} = t;' entonces es una referencia colgante independientemente de si 'operator =' returned 'T &' o 'T const &'. Irónicamente, está bien si el 'operador =' devuelto por el valor! –

+0

@MattMcNabb: Vaya, se suponía que debía decir 'referencia de lvalue'. Gracias por señalarlo (porque sí, una referencia de valor real es * claramente * una mala idea aquí). –

3

No hay ningún requisito lengua de la base del tipo de resultado de un operator=, pero la biblioteca estándar tiene un requisito definido por el usuario:

C++ 98 §23.1/3:

El tipo de objetos almacenados en estos componentes debe cumplir los requisitos de los tipos CopyConstructible (20.1.3) y los requisitos adicionales de los tipos Assignable.

C++ 98 §23.1/4:

En la Tabla 64, T es del tipo utilizado para instanciar el contenedor, t es un valor de T, y u es un valor de (posiblemente const) T.

enter image description here


Volviendo una copia por valor sería todavía apoyar asignación de encadenamiento como a = b = c = 42;, debido a que el operador de asignación es asociativo por la derecha, es decir, esta se analiza como a = (b = (c = 42));. Pero devolver una copia prohibiría construcciones sin sentido como (a = b) = 666;. Para una clase pequeña que regresa una copia podría ser posiblemente la más eficiente, mientras que para una clase más grande que regresa por referencia generalmente será la más eficiente (y una copia, prohibitivamente ineficiente).

Hasta que me enteré del requisito de la biblioteca estándar solía dejar que operator= devolviera void, por eficiencia y para evitar el absurdo de admitir código incorrecto basado en efectos secundarios.


Con C++ 11 existe además el requisito de T& tipo de resultado para default -ing el operador de asignación, porque

C++ 11 §8.4.2/1:

Una función que sea descalificado de forma explícita deberá [& hellip;] tener el mismo tipo de función declarado (a excepción de, posiblemente, diferentes Ref-calificadores y salvo que en el caso de una copia constructor o copiar operador de asignación, el tipo de parámetro pueden ser “referencia a no constante T”, donde T es el nombre de la clase de la función miembro) como si hubiera sido declarado implícitamente

Cuestiones relacionadas