2012-06-06 6 views
5

Se ho una classe A (che restituisce un oggetto di valore), e due funzioni f() eg() con la differenza in appena loro variabili di ritorno:In che modo una funzione restituisce in base al valore?

class A 
{ 
    public: 
    A() { cout<<"constructor, "; } 
    A (const A&) { cout<<"copy-constructor, "; } 
    A& operator = (const A&) { cout<<"assignment, "; } 
    ~A() { cout<<"destructor, "; } 
}; 
    const A f(A x) 
    {A y; cout<<"f, "; return y;} 

    const A g(A x) 
    {A y; cout<<"g, "; return x;} 

main() 
{ 
    A a; 
    A b = f(a); 
    A c = g(a); 
} 

Ora, quando eseguo la linea A b = f(a);, emette:

copy-constructor, constructor, f, destructor, che va bene assumendo che l'oggetto y in f() venga creato direttamente nella destinazione cioè nella posizione di memoria dell'oggetto b, e non vi siano temporaries coinvolti.

Mentre quando eseguo la linea A c = g(a);, essa stampa:

copy-constructor, constructor, g, copy-constructor, destructor, destructor,.

Quindi la domanda è: perché nel caso di g() l'oggetto non può essere creato direttamente nella posizione di memoria di c, come è successo durante il richiamo di f()? Perché chiama un ulteriore copy-constructor (che presumo sia dovuto al coinvolgimento temporaneo) nel secondo caso?

+0

Se si desidera che il compilatore esegua le ottimizzazioni, è necessario compilare con le ottimizzazioni abilitate. –

+0

Non penso che abbia nulla a che fare con le ottimizzazioni del compilatore come ho già provato. – cirronimbo

risposta

3

La differenza è che nel caso g, si restituisce un valore passato alla funzione. Lo standard indica esplicitamente in quali condizioni la copia può essere eliminata in 12.8p31 e non include l'eliminazione della copia da un argomento di funzione.

Fondamentalmente il problema è che la posizione dell'argomento e l'oggetto restituito sono fissati dalla convenzione di chiamata, e il compilatore non può modificare la convenzione di chiamata sulla base del fatto che il attuazione (che potrebbe non essere ancora visibile il luogo di chiamata) restituisce l'argomento.

Ho avviato un blog di breve durata qualche tempo fa (mi aspettavo di avere più tempo ...) e ho scritto un paio di articoli su NRVO e copia elision che potrebbe aiutare a chiarire questo (o no, chi lo sa :)) :

Value semantics: NRVO

Value semantics: Copy elision

+0

Grazie mille. Il tuo "[Un] comportamento definito" ha risolto molti dei miei dubbi in modo certamente "ben definito" :) Ma ho ancora qualche dubbio per te se riesci a provare: 1. In NRVO, quando tu usato "bool che" per decidere da "tipo xey" quale "tipo" restituire, sembra che il mio compilatore non sia in grado di fare l'elision (ne dubito anche altri). – cirronimbo

+0

2. Nel mio codice se lego un riferimento ad un temporaneo vale a dire "A & b = f (a);" allora ciò che accade è che l'ambito dell'oggetto (apparentemente un temporaneo restituito da f (a)) aumenta fino alla parentesi graffa principale. Quindi questo contraddice due cose: 1. Come hai detto nel tuo blog che prendere un indirizzo temporaneo è illegale, ma lo stiamo facendo qui. 2. Come può un temporaneo durare così a lungo? – cirronimbo

+0

3. È così che gli ambiti dell'oggetto membro locale di una funzione e quello dei suoi argomenti sono diversi.Come quando ho fatto qualcosa come "A a; A b; b = g (a);" quindi in linea "b = g (a)" il distruttore dell'oggetto locale è stato chiamato prima dell'operatore di assegnazione e quello dell'argomento, dopo l'assegnazione. – cirronimbo

7

Il problema è che nel secondo caso, si sta restituendo uno dei parametri. Dato che di solito la copia dei parametri si verifica nel sito del chiamante, non all'interno della funzione (main in questo caso), il compilatore esegue la copia, e quindi è costretta a copiarla di nuovo quando entra in g().

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

In secondo luogo, devo ancora trovare un compilatore che elide la copia quando viene restituito un parametro di funzione, come nel nostro attuazione ordinato. Quando pensi a come sono fatte queste elisioni, ha senso: senza una qualche forma di ottimizzazione inter-procedurale, il chiamante di ordinato non può sapere che l'argomento (e non qualche altro oggetto) alla fine verrà restituito, quindi il compilatore deve allocare lo spazio separato nello stack per l'argomento e il valore restituito.

+0

* Devo ancora trovare un compilatore che elide la copia quando viene restituito un parametro di funzione * - Nessuna sorpresa, è impossibile avere una convenzione di chiamata che permetta questo e lo standard (piacevolmente dopo quell'articolo era scritto) afferma esplicitamente che questo non può essere fatto dal compilatore. –

4

Ecco una piccola modifica del codice, che vi aiuterà a capire perfettamente ciò che sta succedendo lì:

class A{ 
public: 
    A(const char* cname) : name(cname){ 
     std::cout << "constructing " << cname << std::endl; 
    } 
    ~A(){ 
     std::cout << "destructing " << name.c_str() << std::endl; 
    } 
    A(A const& a){ 
     if (name.empty()) name = "*tmp copy*"; 
     std::cout 
      << "creating " << name.c_str() 
      << " by copying " << a.name.c_str() << std::endl; 
    } 
    A& operator=(A const& a){ 
     std::cout 
      << "assignment (" 
       << name.c_str() << " = " << a.name.c_str() 
      << ")"<< std::endl; 
     return *this; 
    } 
    std::string name; 
}; 

Ecco l'utilizzo di questa classe:

const A f(A x){ 
    std::cout 
     << "// renaming " << x.name.c_str() 
     << " to x in f()" << std::endl; 
    x.name = "x in f()"; 
    A y("y in f()"); 
    return y; 
} 

const A g(A x){ 
    std::cout 
     << "// renaming " << x.name.c_str() 
     << " to x in f()" << std::endl; 
    x.name = "x in g()"; 
    A y("y in g()"); 
    return x; 
} 

int main(){ 
    A a("a in main()"); 
    std::cout << "- - - - - - calling f:" << std::endl; 
    A b = f(a); 
    b.name = "b in main()"; 
    std::cout << "- - - - - - calling g:" << std::endl; 
    A c = g(a); 
    c.name = "c in main()"; 
    std::cout << ">>> leaving the scope:" << std::endl; 
    return 0; 
} 

ed ecco la uscita compilata senza ottimizzazione:

constructing a in main() 
- - - - - - calling f: 
creating *tmp copy* by copying a in main() 
// renaming *tmp copy* to x in f() 
constructing y in f() 
creating *tmp copy* by copying y in f() 
destructing y in f() 
destructing x in f() 
- - - - - - calling g: 
creating *tmp copy* by copying a in main() 
// renaming *tmp copy* to x in f() 
constructing y in g() 
creating *tmp copy* by copying x in g() 
destructing y in g() 
destructing x in g() 
>>> leaving the scope: 
destructing c in main() 
destructing b in main() 
destructing a in main() 

L'output che hai postato è l'output del programma compilato con Named Return Value Optimization. In questo caso il compilatore tenta di eliminare le chiamate ridondanti del costruttore di copia e del distruttore, il che significa che quando restituisce l'oggetto, proverà a restituire l'oggetto senza crearne una copia ridondante. Ecco l'output con NRVO abilitato:

constructing a in main() 
- - - - - - calling f: 
creating *tmp copy* by copying a in main() 
// renaming *tmp copy* to x in f() 
constructing y in f() 
destructing x in f() 
- - - - - - calling g: 
creating *tmp copy* by copying a in main() 
// renaming *tmp copy* to x in f() 
constructing y in g() 
creating *tmp copy* by copying x in g() 
destructing y in g() 
destructing x in g() 
>>> leaving the scope: 
destructing c in main() 
destructing b in main() 
destructing a in main() 

Nel primo caso, *tmp copy* copiando y in f() non è creato dal NRVO ha fatto il suo lavoro. Nel secondo caso però NRVO non può essere applicato perché un altro candidato per lo slot di ritorno è stato dichiarato all'interno di questa funzione.Per ulteriori informazioni vedere: C++ : Avoiding copy with the "return" statement :)

+0

Sì, lo so e l'ho fatto anche nel mio codice per vedere cosa stava succedendo esattamente (anche se ho pubblicato una versione semplificata del codice enfatizzando solo su quale fosse il mio problema). E questo codice non ha scopo per la domanda che sto chiedendo. Quello che mi è stato chiesto è stata la Ragione per ciò che sta accadendo, non per l'accadimento stesso. Comunque, grazie per mostrare preoccupazione :) – cirronimbo

+0

@cirronimbo: Controlla la mia risposta ora, spiega cosa sta succedendo con NRVO abilitato e spiega anche perché ti ho suggerito questa domanda. – LihO

0

può (quasi) ottimizzare la funzione di chiamata di tutto il g() di distanza, nel qual caso il codice simile a questo:

A a; 
A c = a; 

come questo è ciò che sta facendo il tuo codice. Ora, quando passi il parametro a come parametro di valore (cioè non un riferimento), il compilatore deve quasi eseguire una copia lì, quindi restituisce questo parametro in base al valore, deve eseguire un'altra copia.

Nel caso di f(), poiché restituisce ciò che è effettivamente un temporaneo, in una variabile non inizializzata, il compilatore può vedere che è sicuro utilizzare c come memoria per la variabile interna all'interno di f().