2010-11-02 3 views
10

Sono confuso sullo stato di un oggetto dopo è stato spostato utilizzando semantica di spostamento C++ 0x. La mia comprensione è che una volta che un oggetto è stato spostato, è ancora un oggetto valido, ma il suo stato interno è stato alterato in modo tale che quando viene chiamato il suo distruttore, nessuna risorsa viene deallocata.Oggetti zombi dopo std :: move

Ma se la mia comprensione è corretta, il distruttore di un oggetto spostato dovrebbe essere chiamato.

Ma, che non accade quando effettuo un semplice test:

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)); 
} 

Questa uscita:

Constructor called! 
Doing something... 
Destructor called! 

Avviso il distruttore viene chiamato una sola volta. Questo dimostra che la mia comprensione qui è spenta. Perché il distruttore non è stato chiamato due volte? Ecco la mia interpretazione di ciò che dovrebbe dovuto accadere:

  1. Foo f1 è costruito.
  2. Foo f1 è passato a work, che prende un valore di f2.
  3. Il costruttore mossa di Foo è chiamato, spostando tutte le risorse in f1 a f2.
  4. Ora il distruttore f2 viene chiamato, che rilascia tutte le risorse.
  5. Ora f1 s' distruttore viene chiamato, che in realtà non fa nulla dal momento che tutte le risorse sono state trasferite a f2. Tuttavia, il distruttore è chiamato comunque.

Tuttavia, poiché viene chiamato un solo distruttore, il passaggio 4 o il passaggio 5 non si verificano. Ho fatto un backtrace dal distruttore per vedere da dove è stato invocato, e viene richiamato dal passaggio 5. Quindi perché non viene chiamato anche il distruttore f2?

MODIFICA: OK, ho modificato questo in modo che sia effettivamente in grado di gestire una risorsa. (Un buffer di memoria interno.) Tuttavia, ottengo lo stesso comportamento in cui il distruttore viene chiamato solo una volta.

+0

su quale compilatore stai testando? – jalf

+0

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

+2

In tal caso, vale la pena notare che è scritto contro una versione molto più vecchia del C++ 0x bozza. E spostare la semantica è stata modificata un bel po 'da allora. Ad esempio, credo che un compilatore più recente avrebbe rifiutato del tutto il codice prima di aver aggiunto 'std :: move'. Non sarebbe in grado di chiamare 'work' perché l'argomento era effettivamente un riferimento lvalue perché era chiamato. – jalf

risposta

9

Modifica(Nuovo e risposta corretta)
Siamo spiacenti, guardando più da vicino il codice, sembra che la risposta è molto più semplice: mai richiama il costruttore mossa. Non muovi mai realmente l'oggetto. È sufficiente passare un riferimento di rvalue alla funzione work, che chiama una funzione membro su tale riferimento, che punta ancora all'oggetto originale.

risposta originale, salvato per i posteri

Al fine di eseguire in realtà la mossa, devi avere qualcosa di simile Foo f3(std::move(f2)); all'interno work. Quindi puoi chiamare la funzione membro su f3 che è un nuovo oggetto, creato spostandosi da f

Per quanto posso vedere, non si ottiene affatto la semantica del movimento. Stai solo vedendo una semplice copia di vecchia elisione.

per lo spostamento che si verifica, è necessario utilizzare std::move (o in particolare, l'argomento passato al costruttore deve essere un riferimento di rvalore non denominato/temporaneo), ad esempio quello restituito da std::move). Altrimenti viene trattato come un semplice riferimento lvalue vecchio stile, quindi una copia dovrebbe essere, ma come al solito il compilatore può ottimizzarlo, lasciando che un oggetto venga costruito e un oggetto distrutto.

In ogni caso, anche con la semantica del movimento, non c'è alcun motivo per cui il compilatore non dovrebbe fare la stessa cosa: basta ottimizzare la mossa, proprio come avrebbe ottimizzato la copia. Una mossa è a buon mercato, ma è ancora più economico semplicemente costruire l'oggetto dove serve, piuttosto che costruirne uno, e quindi spostarlo in un'altra posizione e chiamare il distruttore sul primo.

Vale anche la pena notare che si sta utilizzando un compilatore relativamente vecchio e le versioni precedenti delle specifiche non erano molto chiare su cosa dovesse accadere con questi "oggetti zombi". Quindi è possibile che GCC 4.3 non chiami semplicemente il distruttore. Credo che sia solo l'ultima revisione, o forse quella precedente, che richiede esplicitamente che il distruttore sia chiamato

+0

Ok, ho aggiunto una chiamata esplicita a 'std :: move', solo per essere certi che sto davvero passando un rvalue. Ma non cambia il comportamento. – Channel72

+0

Non me lo aspetterei. Per quanto ne so, il compilatore è autorizzato a eseguire copie elision (o spostare elision, immagino) anche su riferimenti di valore: nel tuo semplice caso di test, può semplicemente costruire l'oggetto sul posto, invece di costruirne uno e poi spostarlo esso. – jalf

+0

Hmm ... okay, ho modificato il codice in modo che 'Foo' ora gestisca effettivamente una risorsa. Alloca un buffer e trasferisce la proprietà del buffer nel costruttore di movimento. Tuttavia, il distruttore viene ancora chiamato solo una volta. Sto facendo qualcosa di sbagliato qui? – Channel72

2

Si prega di notare che il compilatore può ottimizzare la costruzione/distruzione non necessaria, rendendo effettivamente un solo oggetto esistente. Questo è particolarmente vero per i riferimenti di valore (che sono stati inventati esattamente per questo scopo).

Penso che tu abbia torto nella tua interpretazione di cosa succede. Il costruttore di movimento non viene chiamato: se il valore non è temporaneo, il riferimento di rvalore si comporta come un riferimento normale.

Forse this article porterà più informazioni sulla semantica di riferimento rvalore.

+0

Anche se non compilo con nessun flag di ottimizzazione? – Channel72

+3

@ Channel72: lo standard C++ non interessa i flag di ottimizzazione. Un'ottimizzazione è legale (rispetta la semantica del C++) o non lo è. E finché è legale, il compilatore può applicarlo quando vuole, per quanto riguarda lo standard C++. Naturalmente, se non è legale, dovrebbe * mai * essere applicato.Il compilatore di solito esegue diverse piccole ottimizzazioni (RVO, ad esempio, e su alcuni compilatori, NRVO) anche con ottimizzazioni disattivate. Probabilmente esegue anche la copia elision, che spiegherebbe i tuoi risultati – jalf

+1

Penso che non ci sia alcuna copia, poiché in realtà non hai alcun oggetto temporaneo nel tuo codice. – Vlad