2010-11-02 9 views
10

Estoy confundido sobre el estado de un objeto después de que se ha movido utilizando la semántica de movimiento C++ 0x. Según tengo entendido, una vez que un objeto se ha movido, sigue siendo un objeto válido, pero su estado interno ha sido alterado de modo que cuando se llama a su destructor, no se desasignan recursos.Objetos de Zombie después de std :: mover

Pero si mi comprensión es correcta, el destructor de un objeto movido debe llamarse.

embargo, que no ocurre cuando realizo una prueba sencilla:

struct Foo 
{ 
    Foo() 
    { 
     s = new char[100]; 
     cout << "Constructor called!" << endl; 
    } 

    Foo(Foo&& f) 
    { 
     s = f.s; 
     f.s = 0; 
    } 

    ~Foo() 
    { 
     cout << "Destructor called!" << endl; 
     delete[] s; // okay if s is NULL 
    } 

    void dosomething() { cout << "Doing something..." << endl; } 

    char* s; 
}; 

void work(Foo&& f2) 
{ 
    f2.dosomething(); 
} 

int main() 
{ 
    Foo f1; 
    work(std::move(f1)); 
} 

Esta salida:

Constructor called! 
Doing something... 
Destructor called! 

Aviso al destructor sólo se le llama una vez. Esto muestra que mi comprensión aquí está apagada. ¿Por qué no se llamó al destructor dos veces? Aquí está mi interpretación de lo que debería han sucedido:

  1. Foo f1 se construye.
  2. Foo f1 se pasa a work, que toma un rvalue f2.
  3. El constructor movimiento de Foo es llamada, moviendo todos los recursos en f1 a f2.
  4. Ahora se llama al destructor f2, liberando todos los recursos.
  5. ahora f1 's destructor se llama, que en realidad no hace nada ya que todos los recursos fueron transferidos a f2. Aún así, el destructor es llamado, no obstante.

Pero como solo se llama a un destructor, el paso 4 o el paso 5 no están sucediendo. Hice una traza inversa desde el destructor para ver desde dónde se estaba invocando, y se está invocando desde el paso 5. Entonces, ¿por qué no se llama también al destructor f2?

EDITAR: Bien, modifiqué esto por lo que en realidad está administrando un recurso. (Un búfer de memoria interna.) Aún así, obtengo el mismo comportamiento donde el destructor solo se llama una vez.

+0

¿en qué compilador está probando? – jalf

+0

gcc 4.3 ......................... – Channel72

+2

En ese caso, vale la pena señalar que está escrito en contra de una versión mucho más antigua de C++ 0x borrador. Y la semántica de movimiento ha cambiado bastante desde entonces. Por ejemplo, creo que un compilador más nuevo habría rechazado su código por completo antes de agregar el 'std :: mover'. No sería capaz de llamar a 'work' porque el argumento era efectivamente una referencia lvalue porque se llamaba. – jalf

Respuesta

9

Editar(Nuevo y respuesta correcta)
Lo sentimos, mirando más de cerca el código, parece que la respuesta es mucho más simple: nunca se invoca el constructor movimiento. En realidad, nunca mueves el objeto. Acaba de pasar una referencia rvalue a la función work, que llama a una función miembro en esa referencia, que aún apunta al objeto original.

Respuesta original, guardado para la posteridad

Para llevar a cabo realmente el movimiento, usted tiene que tener algo así como el interior Foo f3(std::move(f2));work. A continuación, puede llamar a su función de miembro en f3 que es un objeto nuevo, creado al pasar de f

Por lo que puedo ver, no obtiene la semántica de movimiento en absoluto. Solo estás viendo una simple elisión de copia antigua.

para que ocurra el movimiento, debe usar std::move (o específicamente, el argumento que se pasa al constructor tiene que ser una referencia rvalue sin nombre/temporal, como la que se devuelve desde std::move). De lo contrario, se trata como una simple referencia lvalue pasada de moda, y luego una copia debería ocurrir, pero como siempre, el compilador puede optimizarlo, dejándolo con un objeto que se está construyendo y un objeto que se destruye.

De todos modos, incluso con la semántica de movimiento, no hay ninguna razón por la cual el compilador no debería hacer lo mismo: simplemente optimice el movimiento, al igual que hubiera optimizado la copia. Un movimiento es barato, pero aún es más barato construir el objeto donde lo necesita, en lugar de construir uno, y luego moverlo a otra ubicación y llamar al destructor en el primero.

También vale la pena señalar que está utilizando un compilador relativamente antiguo, y las versiones anteriores de las especificaciones no estaban muy claras sobre lo que debería pasar con estos "objetos zombies". Por lo tanto, es posible que GCC 4.3 simplemente no llame al destructor. Creo que es solo la última revisión, o tal vez la anterior, que explícitamente requiere que se llame al destructor

+0

De acuerdo, agregué una llamada explícita a 'std :: move', solo para estar seguro de que estoy realmente pasando un valor de r. Pero no cambia el comportamiento. – Channel72

+0

No lo esperaría. Por lo que sé, el compilador puede realizar elisión de copia (o elisión de movimiento, supongo) en referencias rvalue también: en su caso de prueba simple, puede simplemente construir el objeto en su lugar, en lugar de construir uno y luego moverse eso. – jalf

+0

Hmm ... bien, edité el código para que 'Foo' ahora realmente administre un recurso. Asigna un buffer y transfiere la propiedad del buffer en el constructor de movimientos. Sin embargo, el destructor todavía solo se llama una vez. ¿Estoy haciendo algo mal aquí? – Channel72

2

Tenga en cuenta que el compilador puede optimizar la construcción/destrucción innecesaria, haciendo que solo exista un objeto. Esto es especialmente cierto para las referencias rvalue (que fueron inventadas exactamente para este propósito).

Creo que se equivoca en su interpretación de lo que sucede. El constructor de movimiento no se llama: si el valor no es temporal, la referencia de valor r se comporta como una referencia normal.

Tal vez this article traerá más información sobre la semántica de referencia rvalue.

+0

Incluso si no compilo con ningún indicador de optimización? – Channel72

+3

@ Channel72: el estándar de C++ no se preocupa por los indicadores de optimización. Una optimización es legal (respeta la semántica de C++) o no lo es. Y siempre que sea legal, el compilador puede aplicarlo cuando quiera, en lo que respecta al estándar de C++. Por supuesto, si no es legal, debería * nunca * aplicarse.El compilador generalmente realiza varias optimizaciones pequeñas (RVO, por ejemplo, y en algunos compiladores, NRVO también) incluso con optimizaciones desactivadas. Probablemente también realice la elisión de copia, lo que explicaría sus resultados. – jalf

+1

Creo que no hay copia en absoluto, ya que en realidad no tiene ningún objeto temporal en su código. – Vlad

Cuestiones relacionadas