2010-03-15 38 views
14

Como he entendido, al sobrecargar operador =, el valor de retorno debería ser una referencia no const.Operador de asignación de sobrecarga en C++


A& A::operator=(const A&) 
{ 
    // check for self-assignment, do assignment 

    return *this; 
} 

Es no constante para permitir que las funciones miembro no const a ser llamados en casos como:


(a = b).f(); 

Pero ¿por qué debería devolver una referencia? ¿En qué instancia se generará un problema si el valor de retorno no se declara como referencia, digamos retorno por valor?

Se supone que el constructor de copias está implementado correctamente.

+1

Si quieres que la gente considere la asignación como una declaración en lugar de una expresión, podrías devolver 'void'. Eso detendrá '(a = b) = c',' a = (b = c) 'y cualquier otra chanchullería que pueda haber revelado la diferencia entre un valor y una referencia. –

+0

Encontré útil devolver el vacío en el operador de asignación cuando necesitaba evitar la destrucción automática de objetos a medida que salían de la pila. Para los objetos contados por ref, no quiere que se invoquen destructores cuando no los conoce. – cjcurrie

Respuesta

15

No devolver una referencia es un desperdicio de recursos y produce un diseño extraño. ¿Por qué quiere hacer una copia para todos los usuarios de su operador incluso si casi todos descartan ese valor?

a = b; // huh, why does this create an unnecessary copy? 

Además, sería sorprendente para los usuarios de su clase, ya que el operador de asignación integrada no copia asimismo

int &a = (some_int = 0); // works 
+0

Sí, entiendo que es un desperdicio. Me pregunto si hay casos en que devolver por valor o referencia hace que la operación de asignación sea un valor incorrecto/incorrecto. – jasonline

+0

@Johannes: Lo siento, no recibo tu última oración. ¿Le importaria explicar? – jasonline

+0

@jasonline: obj1 = obj2 devuelve un valor temporal. cuando alguien que use su clase intente crear una referencia a (obj1 = obj2) verá que: 1- no compilará si es una referencia no const, 2- creará una referencia a un objeto temporal (no a obj1 u obj2) que los confundirá, ya que los tipos primitivos no funcionan de esa manera (vea el ejemplo de litb). – tiftik

3

No estoy seguro de la frecuencia con la que querría hacerlo, pero algo como: (a=b)=c; requiere una referencia al trabajo.

Editar: De acuerdo, hay un poco más que eso. Gran parte del razonamiento es semihistórico. Hay más razones por las que no desea devolver un valor r que simplemente evitar una copia innecesaria en un objeto temporal. Usando una variación (menor) en un ejemplo originalmente publicado por Andrew Koenig, considerar algo como esto:

struct Foo { 
    Foo const &assign(Foo const &other) { 
     return (*this = other); 
    } 
}; 

Ahora, supongamos que está utilizando una versión antigua de C++, donde la asignación devuelve un valor de lado derecho. En ese caso, (*this=other); dará ese temporal. Estás vinculando una referencia al temporal, destruyendo el temporal, y finalmente devolviendo una referencia al destruido temporal.

Las reglas que se han promulgado (ampliando la vida de un temporal utilizado para inicializar una referencia) al menos mitigarían (y podrían curar por completo) este problema, pero dudo que alguien volviera a visitar esta situación particular después de que esas reglas sido escrito. Era un poco como un controlador de dispositivo feo que incluye kludges para evitar docenas de errores en diferentes versiones y variaciones del hardware; probablemente podría ser refactorizado y simplificado, pero nadie está seguro de cuándo un cambio aparentemente inocuo romperá algo que actualmente funciona, y en última instancia nadie quiere ni siquiera mirarlo si pueden ayudarlo.

+0

Sí, lo necesita. Pero tienes razón, es extraño codificar así. – jasonline

+1

Escribir (a = b) = c debe ser castigado. –

+0

Hay un montón de programadores "C +" que hacen cosas horribles "inteligentes" como esa. Veo horrores como este tantas veces que siento que estoy viviendo en una película slasher de bajo presupuesto. –

14

Un buen consejo general, cuando la sobrecarga de operadores es 'hacer lo tipos primitivos do ', y el comportamiento predeterminado de asignación a un tipo primitivo es eso.

No devolver nada podría ser una opción, para desactivar la asignación dentro de otras expresiones si lo necesita, pero devolver una copia no tiene ningún sentido: si la persona que llama quiere hacer una copia, puede salir de la referencia, si no necesitan la copia no hay necesidad de generar un temporal que no es necesario.

4

Causa f() puede modificar a.(Volvemos una referencia no const)

Si volvemos un valor (una copia) de un, f() habrá modificar la copia, no un

+0

Sí, estoy de acuerdo también. – jasonline

+0

¿Por qué no hacer que devuelva una referencia constante? De esta forma, no hará una copia y tampoco podrá modificar el objeto devuelto. –

1

En el código real (es decir, no cosas como (a=b)=c), no es probable que devolver un valor cause ningún error de compilación, pero es ineficiente devolver una copia porque la creación de una copia a menudo puede ser costosa.

Obviamente se puede llegar a una situación en la que se necesita una referencia, pero rara vez, si es que alguna vez ocurre, en la práctica.

+0

Sí, es caro. Entiendo tu argumento. – jasonline

0

Si devolvió una copia, sería necesario implementar el constructor de copia para casi todos los objetos no triviales.

También causaría problemas si declara que el constructor de copia es privado pero deja el operador de asignación en público ... obtendría un error de compilación si tratara de usar el operador de asignación fuera de la clase o sus instancias.

Sin mencionar los problemas más graves ya mencionados. No quiere que sea una copia del objeto, realmente quiere que se refiera al mismo objeto. Los cambios en uno deben ser visibles para ambos, y eso no funciona si devuelve una copia.

2

Si su operador de asignación no toma un parámetro de referencia constante:

A& A::operator=(A&); // unusual, but std::auto_ptr does this for example. 

o si la clase A tiene miembros mutables (contador de referencia?), Entonces es posible que el operador de asignación cambia el objeto de ser asignado desde y asignado a. Entonces, si usted tenía un código como éste:

a = b = c; 

La asignación b = c ocurriría primero, y devolver una copia (llámese b') por valor en lugar de devolver una referencia a b. Cuando se realiza la asignación a = b', el operador de asignación de mutación cambiaría la copia b' en lugar del b real.

Otro problema potencial: devolver por valor en lugar de por referencia podría causar cortes si tiene operadores de asignación virtual. No digo que sea una buena idea, pero podría ser un problema.

Si tiene la intención de hacer algo como (a = b).f(), querrá que regrese por referencia para que si f() muta el objeto, no está mutando temporalmente.

1

Si le preocupa que devolver algo incorrecto puede causar silenciosamente efectos secundarios no deseados, puede escribir operator=() para devolver void. He visto un poco de código que hace esto (asumo por pereza o simplemente no saber cuál debería ser el tipo de devolución en lugar de "seguridad"), y causa pocos problemas. El tipo de expresiones que necesitan utilizar la referencia normalmente devuelta por operator=() se usan muy raramente, y casi siempre son fáciles de codificar.

No estoy seguro de que respaldaría devolver void (en una revisión del código, probablemente lo llamaría algo que no debería hacer), pero lo estoy considerando como una opción para considerar si no quiere preocuparse por cómo se manejarán los usos extraños del operador de asignación.


fines de edición:

Además, debo tener inicialmente mencionado que se puede dividir la diferencia por tener su operator=() devolver un const& - que todavía permita la asignación de encadenamiento:

a = b = c; 

Pero deshabilitará algunos de los usos más inusuales:

(a = b) = c; 

Tenga en cuenta que esto hace que el operador de asignación tenga una semántica similar a la que tiene en C, donde el valor devuelto por el operador = no es un valor l. En C++, el estándar lo cambió para que el operador = devuelva el tipo del operando izquierdo, por lo que es un valor l, pero como Steve Jessop señaló en un comentario a otra respuesta, mientras que eso hace que el compilador acepte

(a = b) = c; 

incluso para incorporados, el resultado es un comportamiento indefinido para los integradores ya que a se modifica dos veces sin ningún punto de secuencia intermedio. Ese problema se evita para los builtins con operator=() porque la llamada a la función operator=() es un punto de secuencia.

+0

@Michael: Gracias por la explicación adicional (y clara) sobre la diferencia en C y C++, y los puntos de secuencia. Nunca pensé que hubiera una diferencia. De todos modos, me preocupa cómo implementarlo de la manera correcta (como lo hacen los primitivos) y por qué implementarlo de esa manera. No tengo ninguna intención de tener que devolver el vacío, ya que desactivaría el encadenamiento, que normalmente debería permitirse. – jasonline

1

Este es el artículo 10 del excelente libro de Scott Meyers, Effective C++. Devolver una referencia de operator= es solo una convención, pero es buena.

Esto es solo una convención; el código que no lo sigue se compilará. Sin embargo, la convención es seguida por todos los tipos incorporados, así como por todos los tipos en la biblioteca estándar. A menos que tenga una buena razón para hacer las cosas de manera diferente, no lo haga.

+0

Sí, he leído sobre esto. De hecho, estaba buscando alguna instancia en la que causaría un valor incorrecto, pero supongo que la mayoría de las respuestas son problemas de eficiencia. – jasonline

0

La devolución por referencia reduce el tiempo de realización de operaciones encadenadas. P.ej. :

a = b = c = d; 

Vamos a ver las acciones que se llamaría, si operator= rendimientos en términos de valor.

  1. de asignación de copia Opertor = para cc hace igual a d y luego crea temporal anónima objeto (llamadas copiar ctor). Vamos a llamarlo tc.
  2. Luego se llama operador = para b. El objeto del lado derecho es tc. Se llama al operador mover asignación. b pasa a ser igual a tc. Y luego la función copia b a anónimo temporal, llamémoslo tb.
  3. Lo mismo ocurre nuevamente, a.operator= devuelve una copia temporal de a.Después de operador ; los tres objetos temporales se destruyen

En total: 3 ctors copia, 2 operadores de movimiento, 1 operador de copia

Vamos a ver qué va a cambiar si el operador = valor volverá por referencia:

  1. Se llama al operador de asignación de copias. c se convierte en igual a d, se devuelve la referencia al objeto lvalue
  2. Lo mismo. b se convierte en igual a c, se devuelve la referencia al objeto lvalue
  3. Lo mismo. a se hace igual a b, la referencia a lValue objeto se devuelve

En total: sólo tres operadores de copia se llama y no hay ctors en todo!

Por otra parte Te recomiendo que devuelvas el valor por const reference, no te permitirá escribir código engañoso y obvio. Con código más limpio encontrar errores será mucho más fácil :) (a = b).f(); es mejor dividir en dos líneas a=b; a.f();.

P. S.: Operador de asignación de copias: operator=(const Class& rhs).

Operador de asignación de movimiento: operator=(Class&& rhs).