Objetos temporales: cuando se crean, ¿cómo se los reconoce en el código?

En Eckel, Vol 1, pg: 367

//: C08:ConstReturnValues.cpp // Constant return by value // Result cannot be used as an lvalue class X { int i; public: X(int ii = 0); void modify(); }; X::X(int ii) { i = ii; } void X::modify() { i++; } X f5() { return X(); } const X f6() { return X(); } void f7(X& x) { // Pass by non-const reference x.modify(); } int main() { f5() = X(1); // OK -- non-const return value f5().modify(); // OK // Causes compile-time errors: //! f7(f5()); //! f6() = X(1); //! f6().modify(); //! f7(f6()); } ///:~ 

¿Por qué fracasa f5() = X(1) ? ¿¿¿Que esta pasando aqui???

Q1. Cuando hace X(1) , ¿qué está pasando aquí? ¿Es esto una llamada de constructor? ¿No debería leer X::X(1); ¿Es instanciación de clase? ¿No es una instanciación de clase algo así como: X a(1); ¿Cómo determina el comstackdor qué es X(1) ? Quiero decir … la decoración del nombre tiene lugar así que … X(1) la llamada del constructor se traduciría a algo así como: globalScope_X_int como el nombre de la función … ???

Q2. Sin duda, un objeto temporal se usa para almacenar el objeto resultante que X(1) crea y luego no se le asignará al objeto f5() (que también sería un objeto temporal). Dado que f5() devuelve un objeto temporal que pronto será descartado, ¿cómo puede asignar una constante temporal a otra constante temporal? ¿Podría alguien explicar claramente por qué: f7(f5()); debería repetirse en una constante temporal y no simple viejo f5();

Todas sus preguntas se reducen a una regla en C ++ que dice que un objeto temporal (uno que no tiene nombre) no puede vincularse a una referencia no constante. (Debido a que Stroustrup sintió que podría provocar errores lógicos …)

La única pega es que puedes invocar un método en un temporal: entonces X(1).modify() está bien pero f7(X(1)) no lo es.

En cuanto a dónde se crea el temporal, este es el trabajo del comstackdor. Las reglas del lenguaje precisan que el temporal solo debe sobrevivir hasta el final de la expresión completa actual (y no más) que es importante para instancias temporales de clases cuyo destructor tiene un efecto secundario.

Por lo tanto, la siguiente statement X(1).modify(); puede traducirse completamente a:

 { X __0(1); __0.modify(); } // automatic cleanup of __0 

Con eso en mente, podemos atacar f5() = X(1); . Tenemos dos temporales aquí, y una asignación. Ambos argumentos de la asignación deben evaluarse por completo antes de invocar la asignación, pero el orden no es preciso. Una posible traducción es:

 { X __0(f5()); X __1(1); __0.operator=(__1); } 

( la otra traducción está intercambiando el orden en que __0 y __1 están inicializados )

Y la clave para que funcione es que __0.operator=(__1) es una invocación al método, y los métodos pueden invocarse en los temporales 🙂

Las respuestas no me satisficieron del todo, así que eché un vistazo a:

“C ++ más efectivo”, Scott Meyers. Ítem ​​19: “Comprender el origen de los objetos temporales”

. Con respecto a la cobertura de Bruce Eckel de “Temporarios”, bueno, como sospecho y como Christian Rau señala directamente, ¡está claro que está mal! Grrr! ¡Está (Eckel) usándonos como conejillos de Indias! (Sería un buen libro para los novatos como yo una vez que corrige todos sus errores)

Meyer: “Los verdaderos objetos temporales en C ++ son invisibles, no aparecen en el código fuente. Surgen cada vez que se crea un objeto que no es de montón pero no se nombra. Tales objetos sin nombre generalmente surgen en una de dos situaciones: cuando las conversiones de tipo implícito se aplican para que las llamadas a funciones tengan éxito y cuando las funciones devuelvan objetos “.

“Considere primero el caso en el que se crean objetos temporales para que las llamadas a funciones tengan éxito. Esto sucede cuando el tipo de objeto que se pasa a una función no es el mismo que el tipo del parámetro al que se está enlazando”.

“Estas conversiones se producen solo al pasar objetos por valor o al pasar a un parámetro de referencia a constelación. No ocurren cuando se pasa un objeto a un parámetro de referencia a no const”.

“El segundo conjunto de circunstancias bajo el cual se crean objetos temporales es cuando una función devuelve un objeto”.

“Cada vez que ve un parámetro de referencia a const, existe la posibilidad de que se cree un temporal para enlazar a ese parámetro. Cada vez que vea una función que devuelve un objeto, se creará un temporal (y luego se destruirá)”.

La otra parte de la respuesta se encuentra en: “Meyer: Effective C ++”, en la “Introducción”:

“un constructor de copia se usa para inicializar un objeto con un objeto diferente del mismo tipo:”

 String s1; // call default constructor String s2(s1); // call copy constructor String s3 = s2; // call copy constructor 

“Probablemente el uso más importante del constructor de copias es definir qué significa pasar y devolver objetos por valor”.

En cuanto a mis preguntas:

 f5() = X(1) //what is happening? 

Aquí no se está inicializando un nuevo objeto, por lo que no se trata de inicialización (constructor de copia): es una tarea (como señaló Matthieu M).

Los temporales se crean porque según Meyer (párrafos superiores), ambas funciones devuelven valores, por lo que se crean objetos temporales. Como Matthieu señaló utilizando pseudo-código, se convierte en: __0.operator=(__1) y se lleva a cabo una copia bit a bit (realizada por el comstackdor).

Respecto a:

 void f7(X& x); f7(f5); 

ergo, no se puede crear un temporal (Meyer: párrafos superiores). Si se hubiera declarado: void f7(const X& x); entonces se habría creado un temporal.

En cuanto a un objeto temporal que es una constante:

Meyer lo dice (y Matthieu): “se creará un temporal para enlazar a ese parámetro”.

Entonces, un temporal solo está vinculado a una referencia constante y no es un objeto “const”.

En cuanto a: ¿qué es X(1) ?

Meyer, artículo27, efectivo C ++ – 3e, dice:

“Los moldes de estilo C se ven así: (T) expresión // expresión de fundición para ser del tipo T

Los moldes de estilo de función usan esta syntax: T (expresión) // expresión de molde para que sea de tipo T ”

Entonces X(1) es un molde de estilo funcional. 1 la expresión está siendo convertida a tipo X

Y Meyer lo dice de nuevo:

“La única vez que uso un modelo antiguo es cuando quiero llamar a un constructor explícito para pasar un objeto a una función. Por ejemplo:

 class Widget { public: explicit Widget(int size); ... }; void doSomeWork(const Widget& w); doSomeWork(Widget(15)); //create Widget from int //with function-style cast doSomeWork(static_cast(15)); 

De alguna manera, la creación deliberada de objetos no se “siente” como un yeso, por lo que probablemente usaría el molde de estilo de función en lugar del static_cast en este caso “.

  1. Esto es de hecho una llamada de constructor, una expresión que evalúa a un objeto temporal de tipo X Las expresiones de la forma X([...]) donde X es el nombre de un tipo son llamadas de constructor que crean objetos temporales de tipo X (aunque no sé cómo explicarlo en un estándar propio, y hay casos especiales) donde el analizador puede comportarse de manera diferente). Esta es la misma construcción que usas en tus funciones f5 y f6 , simplemente omitiendo el argumento ii opcional.

  2. El temporal creado por X(1) vive (no se destruye / invalida) hasta el final de la expresión completa que lo contiene, lo que normalmente significa (como en este caso con la expresión de asignación) hasta el punto y coma. Del mismo modo, f5 crea una X temporal y la devuelve al sitio de la llamada (dentro de la main ) y la copia. Entonces, en main, la llamada f5 también devuelve una X temporal. A esta X temporal se le asigna la X temporal creada por X(1) . Una vez hecho esto (y se llega al punto y coma, si lo desea), ambos temporales se destruyen. Esta asignación funciona porque esas funciones devuelven objetos ordinarios no constantes , no importa si son solo temporales y se destruyen después de que la expresión se evalúa por completo (lo que hace que la tarea sea más o menos insensata, aunque sea perfectamente válida).

    No funciona con f6 ya que devuelve una const X sobre la cual no puede asignar. Del mismo modo, f7(f5()) no funciona, ya que f5 crea un objeto temporal y temporal que no se une a referencias de valores no const X& (C ++ 11 introdujo referencias de valor X&& para este propósito, pero esa es una historia diferente). Funcionaría si f7 tomara una const referencia const X& , ya que las referencias constantes de lvalue se unirían a las temporales (pero entonces f7 sí mismo ya no funcionaría, por supuesto).

Aquí hay un ejemplo de lo que sucede realmente cuando ejecuta su código. He hecho algunas modificaciones para aclarar los procesos detrás de la escena:

 #include  struct Object { Object( int x = 0 ) {std::cout << this << ": " << __PRETTY_FUNCTION__ << std::endl;} ~Object() {std::cout << this << ": " << __PRETTY_FUNCTION__ << std::endl;} Object( const Object& rhs ){std::cout << this << ": " << __PRETTY_FUNCTION__ << " rhs = " << &rhs << std::endl;} Object& operator=( const Object& rhs ) { std::cout << this << ": " << __PRETTY_FUNCTION__ << " rhs = " << &rhs << std::endl; return *this; } static Object getObject() { return Object(); } }; void TestTemporary() { // Output on my machine //0x22fe0e: Object::Object(int) -> The Object from the right side of = is created Object(); //0x22fdbf: Object::Object(int) -> In getObject method the Temporary Unnamed object is created //0x22fe0f: Object::Object(const Object&) rhs = 0x22fdbf -> Temporary is copy-constructed from the previous line object //0x22fdbf: Object::~Object() -> Temporary Unnamed is no longer needed and it is destroyed //0x22fe0f: Object& Object::operator=(const Object&) rhs = 0x22fe0e -> assignment operator of the returned object from getObject is called to assigne the right object //0x22fe0f: Object::~Object() - The return object from getObject is destroyed //0x22fe0e: Object::~Object() -> The Object from the right side of = is destroyed Object(); Object::getObject() = Object(); } 

Debe saber que en la mayoría de los comstackdores modernos se evitará la construcción de copias. Esto se debe a la optimización realizada por el comstackdor (Return Value Optimization). En mi salida, eliminé explícitamente la optimización para mostrar qué sucede realmente de acuerdo con el estándar. Si también desea eliminar esta optimización, use la siguiente opción:

 -fno-elide-constructors