2010-01-09 20 views
6

Recientemente he leído (y por desgracia recuerdo donde), que la mejor manera de escribir operador = es la siguiente:copia explícita constructor o parámetro implícito por valor

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

en lugar de esto:

foo &operator=(const foo &other) 
{ 
    foo copy(other); 
    swap(*this, copy); 
    return *this; 
} 

La idea es que si operator = se llama con un valor r, la primera versión puede optimizar la construcción de una copia. Entonces cuando se llama con un valor r, la primera versión es más rápida y cuando se llama con un lvalue los dos son equivalentes.

Tengo curiosidad por saber qué piensan los demás acerca de esto? ¿La gente evitaría la primera versión por falta de claridad? ¿Estoy en lo correcto al decir que la primera versión puede ser mejor y que nunca puede ser peor?

+0

¿Qué es 'swap'? Si es 'foo temp = x; x = y; y = temp; 'tiene la recursión infinte de la función' operator = 'y' swap'. –

+1

Escribí un programa para probar la teoría de @Alexey Malistov y él estaba en lo correcto: obtuve recurrencia infinita. – nobar

+0

Cualquier clase que implemente un modificador de copia y de intercambio no puede confiar en la implementación 'std :: swap' predeterminada en su operador de asignación de copias. Eso prácticamente no hace falta decirlo. –

Respuesta

4

Probablemente leerlo desde: http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/

no tengo mucho que decir ya que creo que el enlace explica la lógica bastante bien. Anecdóticamente puedo confirmar que el primer formulario da como resultado menos copias en mis compilaciones con MSVC, lo que tiene sentido ya que los compiladores podrían no ser capaces de copiar elisión en el segundo formulario. Estoy de acuerdo en que la primera forma es una mejora estricta y nunca es peor que la segunda.

Editar: La primera forma puede ser un poco menos idiomática, pero no creo que esté mucho menos clara. (IMO, no es más sorprendente que ver la implementación de copiar y cambiar del operador de asignación por primera vez).

Edición n.º 2: Vaya, quise escribir copia elisión, no RVO.

+1

No estoy de acuerdo con que el primero sea idiomático. Esta es la versión menos común. Creo que si trabajas para una gran empresa que hace muchas revisiones de código, te enviarán de vuelta a hacer de la forma habitual. Lo normal es mejor para el mantenimiento ya que las personas no necesitan hacer una doble inspección para resolver lo que está sucediendo. Creo que la primera versión es solo un buen truco de fiesta para mostrar lo genial que eres. –

+1

No dije exactamente que la primera forma era idiomática (y sí dije que la segunda forma era más). Pero las expresiones idiomáticas tienen que comenzar en algún lado, y sé que la segunda forma me hizo hacer una doble toma la primera vez que la vi. – jamesdlin

+1

@Martin: como ha dicho Herb Sutter, cuando la convención entra en conflicto con las buenas prácticas, debemos preguntarnos si debemos descartar las convenciones a favor de las buenas prácticas o desechar las buenas prácticas a favor de las convenciones. –

-3

creo que podría estar confundiendo la diferencia entre:

foo &operator=(const foo &other); y
const foo &operator=(const foo &other);

La primera forma debe ser usado para permitir: (a = b) = c;

+4

No creo que los confunda, su pregunta tiene mucho sentido. –

-2

estos dos son en realidad la misma. La única diferencia es donde presionas "Step In" en un depurador. Y debe saber de antemano dónde hacerlo.

2

Generalmente prefiero la segunda desde la legibilidad y el punto de vista de la "menos sorpresa"; sin embargo, reconozco que la primera puede ser más eficiente cuando el parámetro es temporal.

La primera realmente puede dar lugar a no copias, no solo una copia y es concebible que esto pueda ser una preocupación genuina en situaciones extremas.

E.g. Toma este programa de prueba. gcc -O3 -S (gcc versión 4.4.2 20091222 (Red Hat 4.4.2-20) (GCC)) genera una llamada al constructor de copias de B pero no llamadas al constructor de copias de A para la función f (el operador de asignación está en línea tanto para A como para B) A y B pueden tomarse como clases de cuerdas muy básicas. La asignación y el copiado para data se producirían en los constructores y la desasignación en el destructor.

#include <algorithm> 

class A 
{ 
public: 
    explicit A(const char*); 
    A& operator=(A val)  { swap(val); return *this; } 
    void swap(A& other)  { std::swap(data, other.data); } 
    A(const A&); 
    ~A(); 

private: 
    const char* data; 
}; 

class B 
{ 
public: 
    explicit B(const char*); 
    B& operator=(const B& val) { B tmp(val); swap(tmp); return *this; } 
    void swap(B& other)   { std::swap(data, other.data); } 
    B(const B&); 
    ~B(); 

private: 
    const char* data; 
}; 

void f(A& a, B& b) 
{ 
    a = A("Hello"); 
    b = B("World"); 
} 
+0

Es notable que ninguna clase tiene ningún miembro de datos. Además, ¿podría editar su ejemplo para minimizar un poco el espacio en blanco vertical? –

+0

@Neil: Los miembros de datos no hacen diferencia, acabo de agregar un 'datos largos [32];' a ambas estructuras en mi arnés de prueba. En teoría, el miembro de datos podría ser tocado por los constructores que he dejado indefinido. Las llamadas generadas a los constructores se mantienen igual, una llamada a 'B :: B (const B &)', cero llamadas a 'A :: A (const A &)'. –

+0

Ah, creo que entiendo a qué te refieres. Agregué otro miembro de datos del tipo de clase (clase C) con el constructor declarado, pero no definido, el constructor de copia, el destructor y un miembro de datos void *. Aunque se generó un código adicional de manejo de excepciones, y algunas llamadas a 'C :: ~ C()' en los lugares esperados, la diferencia entre llamar 'B :: B (const B &)' y 'A :: A (const A &) '. Si esto marca alguna diferencia en el mundo real está abierto al debate, pero la posibilidad está ciertamente ahí. –

0

Dada esta

foo &foo::operator=(foo other) {/*...*/ return *this;} 
foo f(); 

en código como este

foo bar; 
bar = f(); 

Podría ser más fácil para un compilador para eliminar la llamada al constructor de copia. Con RVO, podría usar la dirección del parámetro other del operador como el lugar para f() para construir su valor de retorno en.

Parece que esta optimización también es posible para el segundo caso, aunque creo que podría ser más difícil. (Especialmente cuando el operador no está en línea.)

Cuestiones relacionadas