2012-06-06 12 views
5

Si tengo una clase A (que devuelve un objeto por valor), y dos funciones f() y g() que tiene una diferencia en tan sólo sus variables de retorno:¿Cómo realmente devuelve una función por valor?

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

Ahora cuando ejecuto la línea de A b = f(a);, Emite:

copy-constructor, constructor, f, destructor, lo que está bien suponiendo que el objeto y en f() se crea directamente en el destino, es decir, en la ubicación de memoria del objeto b, y no hay temporales involucrados.

Mientras que cuando ejecuto la línea A c = g(a);, se da salida:

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

Entonces, la pregunta es por qué en el caso de g() no se puede crear el objeto directamente en la ubicación de la memoria de c, tal como sucedió al llamar a f()? ¿Por qué llama a un copiador-constructor adicional (que presumo es debido a la participación temporal) en el segundo caso?

+0

Si desea que el compilador realice optimizaciones, deberá compilar con las optimizaciones habilitadas. –

+0

No creo que tenga nada que ver con las optimizaciones del compilador, como ya lo he intentado. – cirronimbo

Respuesta

3

La diferencia es que en el caso g, está devolviendo un valor que se pasó a la función. El estándar establece explícitamente bajo qué condiciones se puede elidir la copia en 12.8p31 y no incluye eludir la copia de un argumento de función.

Básicamente el problema es que la ubicación del argumento y el objeto devuelto son fijados por la convención de llamada, y el compilador no puede cambiar la convención de llamada basada en el hecho de que el aplicación (que podría incluso no ser visible en el lugar de llamada) devuelve el argumento.

Empecé un blog de corta vida hace un tiempo (esperaba tener más tiempo ...) y escribí un par de artículos sobre NRVO y elisión de copia que podrían ayudar a aclarar esto (o no, quién sabe :)) :

Value semantics: NRVO

Value semantics: Copy elision

+0

Muchas gracias. Su "comportamiento [Un] definido" resolvió muchas de mis dudas en una manera "bien definida" :) Pero tengo algunas dudas más si puede decirlas: 1. En NRVO, cuando usé "bool which" para decidir "type x and y" que "type" para devolver, parece que mi compilador no puede hacer la elisión (dudo que otros también) – cirronimbo

+0

2. En mi código si ato una referencia a una temporal, es decir, "A & b = f (a);" entonces lo que sucede es que el alcance del objeto (supuestamente un temporal devuelto por f (a)) aumenta hasta la llave final de la principal. Entonces, esto contradice dos cosas: 1. Como mencionaste en tu blog que tomar una dirección temporal es ilegal, pero lo estamos haciendo aquí. 2. ¿Cómo puede un temporal durar tanto tiempo? – cirronimbo

+0

3. ¿Es así que los ámbitos de los objetos miembros locales de una función y sus argumentos son diferentes?Como cuando hice algo como "A a; A b; b = g (a);" luego, en la línea "b = g (a)", el destructor del objeto local se llamó antes del operador de asignación y el del argumento, después de la asignación. – cirronimbo

7

El problema es que en el segundo caso, está devolviendo uno de los parámetros. Dado que, por lo general, la copia de parámetros se produce en el sitio de la persona que llama, no dentro de la función (main en este caso), el compilador realiza la copia y luego la fuerza a copiarla una vez que ingresa g().

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

En segundo lugar, todavía tengo que encontrar un compilador que va a eludir la copia cuando se devuelve un parámetro de función, al igual que en nuestra implementación de la ordenada. Cuando piensas en cómo se hacen estas elisiones, tiene sentido: sin alguna forma de optimización interprocedimiento, el llamador de ordenado no puede saber que el argumento (y no algún otro objeto) finalmente será devuelto, por lo que el compilador debe asigna espacio separado en la pila para el argumento y el valor de retorno.

+0

* Todavía no he encontrado un compilador que elimine la copia cuando se devuelve un parámetro de función * - No hay sorpresas allí, es imposible tener una convención de llamadas que permita esto, y el estándar (agradablemente después de ese artículo era escrito) declara explícitamente que esto no puede ser hecho por el compilador. –

4

He aquí una pequeña modificación de su código, que le ayudará a entender perfectamente lo que está pasando allí:

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

Aquí está el uso de esta clase:

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

y aquí está la salida compilada sin ninguna optimización:

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() 

La salida que ha publicado es la salida del programa compilado con Named Return Value Optimization. En este caso , el compilador intenta eliminar el constructor copia redundante y las llamadas Destructor, lo que significa que al devolver el objeto, intentará devolver el objeto sin crear una copia redundante del mismo. Aquí está la salida con NRVO permitido:

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() 

En primer caso, *tmp copy* copiando y in f() no se crea desde NRVO ha hecho su trabajo. En el segundo caso, aunque NRVO no se puede aplicar porque se ha declarado otro candidato para la ranura de devolución dentro de esta función.Para obtener más información, consulte: C++ : Avoiding copy with the "return" statement :)

+0

Sí, lo sé y también lo he hecho en mi código para ver qué estaba sucediendo exactamente (aunque publiqué una versión simplificada del código que hacía hincapié solo en cuál era mi problema). Y este código no sirve para la pregunta que estoy haciendo. Lo que se me preguntó fue la RAZÓN de lo que está sucediendo, no el suceso en sí mismo. De todos modos, gracias por mostrar preocupación :) – cirronimbo

+0

@cirronimbo: compruebe mi respuesta ahora, explica qué está pasando con NRVO habilitado y también explica por qué le sugerí esa pregunta. – LihO

0

puede (casi) optimizar la función de llamada entera g() de distancia, en cuyo caso el código es el siguiente:

A a; 
A c = a; 

como efectivamente esto es lo que su código está haciendo. Ahora, al pasar a como un parámetro de valor por valor (es decir, no una referencia), entonces el compilador casi tiene que realizar una copia allí, y luego devuelve este parámetro por valor, tiene que realizar otra copia.

En el caso de f(), ya que devuelve lo que es efectivamente un temporal, en una variable no inicializada, el compilador puede ver que es seguro usar c como almacenamiento para la variable interna dentro de f().

Cuestiones relacionadas