2010-04-26 16 views
6

(estoy usando gcc con -O2.)¿Por qué no se elimina el constructor de copia aquí?

Esto parece una oportunidad directa para eludir el constructor de copia, ya que no hay efectos secundarios a acceder al valor de un campo en una copia de un bar 's foo; pero el constructor de copia es llamado, ya que obtengo la salida meep meep!.

#include <iostream> 

struct foo { 
    foo(): a(5) { } 
    foo(const foo& f): a(f.a) { std::cout << "meep meep!\n"; } 
    int a; 
}; 

struct bar { 
    foo F() const { return f; } 
    foo f; 
}; 

int main() 
{ 
    bar b; 
    int a = b.F().a; 
    return 0; 
} 
+0

¿Está preguntando por qué se llama al constructor de copia en lugar de simplemente devolver el valor del campo de la instancia original? –

+1

¿Cómo sabes que no fue eliminado? ¿Y por qué crees que debería haber sido? Por favor, modifique su pregunta con respecto a estas dos consultas. –

+0

@Michael, eso es correcto. –

Respuesta

11

Es ninguno de los dos casos legales de la elisión copia ctor descritos en 12.8/15:

optimización Valor de retorno (en el que se devuelve una variable automática de una función, y la copia de esa automático para el valor de retorno se elimina al construir la automática directamente en el valor de retorno) - no. f no es una variable automática.

Inicializador temporal (donde se copia un temporal a un objeto, y en lugar de construir el temporal y copiarlo, el valor temporal se construye directamente en el destino) - no f tampoco es temporal. b.F() es temporal, pero no se copia en ninguna parte, solo tiene acceso a un miembro de datos, por lo que cuando salga de F() no hay nada que pueda evitar.

Dado que ninguno de los casos legales de las manzanas elisión copia ctor, y la copia de f que el valor de retorno de F() afecta al comportamiento observable del programa, la norma prohíbe que pueda ser elidido. Si reemplazó la impresión con alguna actividad no observable y examinó el ensamblaje, es posible que vea que este constructor de copia se ha optimizado. Pero eso estaría bajo la regla "como si", no bajo la regla de elisión del constructor de copias.

+1

Interesante, gracias. Aunque, ¿por qué el estándar no permite este caso? –

+0

Por la misma razón que no permite omitir cualquier función anterior, que contiene una llamada a 'std :: cout <<'. La gran pregunta es, ¿por qué el estándar * ever * permite "optimizaciones" que cambian el comportamiento observable del programa? La respuesta es que se consideró necesario evitar cadenas ridículas de copia de valores temporales y de retorno, y que nadie querría confiar en que se realizara la copia en esos dos casos. Si quiere evitar copiar en su caso, puede devolver un 'const foo &'. En los dos casos legales de copia ctor elision, no puede evitar una copia de esa manera. –

+0

Así que sospecho que la razón por la cual no se agregó ningún caso de elisión para cubrir su ejemplo, es que puede hacerlo usted mismo, y en ausencia de necesidad, los casos especiales que rompen el comportamiento son malos. Sin embargo, es solo una suposición. –

1

El constructor de copia se llama porque a) no hay garantía de que va a copiar el valor del campo sin modificaciones, y b) porque su constructor de copia tiene un efecto secundario (imprime un mensaje).

+3

Los constructores de copia con efectos secundarios pueden ser eliminados. En pocas palabras, no escriba constructores de copia como ese. –

1

Una mejor manera de pensar acerca de copiar elision es en términos del objeto temporal. Así es como el estándar lo describe. Se permite que un temporal se "doble" en un objeto permanente si se copia en el objeto permanente inmediatamente antes de su destrucción.

Aquí construyes un objeto temporal en la función return. En realidad, no participa en nada, por lo que desea que se omita. Pero lo que si se hubiera hecho

b.F().a = 5; 

si se elide la copia, y operado en el objeto original, que habría modificado a través de una b no es de referencia.

+0

Pero el compilador solo puede elidear la copia si no se usa como valor l. –

+0

@Jesse: en mi ejemplo, no se usa como lvalue. Se usa como el lado izquierdo del operador '.'. – Potatoswatter

+0

@Potatoswatter - pero luego el resultado del operador '.' se usa como valor l. No estoy 100% seguro acerca de mi terminología en C++, así que tal vez lvalue no es la palabra correcta, pero el concepto que estoy buscando debe ser transitivo. –

2

La elisión de copia ocurre solo cuando una copia no es realmente necesaria. En particular, es cuando hay un objeto (llámelo A) que existe durante la ejecución de una función, y un segundo objeto (llámelo B) que se copiará desde el primer objeto, y inmediatamente después , A se destruirá (es decir, al salir de la función).

En este caso muy específico, la norma otorga permiso para que el compilador combine A y B en dos formas distintas de referirse al mismo objeto.En lugar de requerir que A sea creado, entonces B será una copia construida desde A, y luego A será destruida, permite que A y B se consideren dos formas de referirse al mismo objeto, por lo que el (uno) objeto se crea como A, y después de que la función devuelve comienza a llamarse B, pero incluso si el constructor de la copia tiene efectos secundarios, la copia que crea B de A todavía puede omitirse. Además, tenga en cuenta que en este caso A (como un objeto separado de B) tampoco se destruye, por ejemplo, si su dtor también tuviera efectos secundarios, también podrían (podrían) omitirse.

Su código no se ajusta a ese patrón: el primer objeto no deja de existir inmediatamente después de ser utilizado para inicializar el segundo objeto. Después de F() devuelve, hay dos instancias del objeto. Siendo ese el caso, la Optimización del Valor de Retorno [Nombrado] (también conocida como copia elisión) simplemente no se aplica.

código de demostración cuando se aplicaría copia elisión:

#include <iostream> 

struct foo { 
    foo(): a(5) { } 
    foo(const foo& f): a(f.a) { std::cout << "meep meep!\n"; } 
    int a; 
}; 

int F() { 
    // RVO 
    std::cout << "F\n"; 
    return foo(); 
} 

int G() { 
    // NRVO 
    std::cout << "G\n"; 
    foo x; 
    return x; 
} 

int main() { 
    foo a = F(); 
    foo b = G(); 
    return 0; 
} 

Tanto MS VC++ y g ++ optimizar la distancia tanto copiar ctors de este código con la optimización activada. g ++ optimiza ambas cosas, incluso si la optimización está desactivada. Con la optimización desactivada, VC++ optimiza la devolución anónima, pero utiliza el copiador para la devolución especificada.

Cuestiones relacionadas