c++ - Objetos temporales: cuando se crean, ¿cómo los reconoces en el código?
temporary-objects (4)
En Eckel, Vol. 1, pág. 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é f5() = X(1)
éxito? ¿¿¿Que esta pasando aqui???
Q1. Cuando hace X(1)
, ¿qué está pasando aquí? ¿Es esta una llamada de un constructor? ¿No debería leer X::X(1);
¿Es una instanciación de clase? ¿No es una creación de instancias de clase algo como: X a(1);
¿Cómo determina el compilador 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 como: globalScope_X_int
como el nombre de la función ...
Q2. Seguramente se usa un objeto temporal para almacenar el objeto resultante que X(1)
crea y luego, ¿no sería así asignado al objeto f5()
devuelve (que también sería un objeto temporal)? Dado que f5()
devuelve un objeto temporal que pronto se descartará, ¿cómo puede asignar una constante temporal a otra constante temporal? ¿Podría alguien explicar claramente por qué: f7(f5());
debe volver en una constante temporal y no simple f5();
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 formaX([...])
conX
como nombre de un tipo son llamadas de constructor que crean objetos temporales de tipoX
(aunque no sé cómo explicarlo en la normalización adecuada, y hay casos especiales) donde el analizador puede comportarse de manera diferente). Esta es la misma construcción que utiliza en sus funcionesf5
yf6
, simplemente omitiendo el argumentoii
opcional.El temporal creado por
X(1)
vive (no se destruye / no es válido) hasta el final de la expresión completa que lo contiene, lo que generalmente significa (como en este caso con la expresión de asignación) hasta el punto y coma. Del mismo modo,f5
crea unaX
temporal y la devuelve al sitio de la llamada (dentro demain
), por lo que la copia. Así que en main la llamadaf5
también devuelve unaX
temporal. A estaX
temporal se le asigna laX
temporal creada porX(1)
. Después de que se hace (y se alcanza el punto y coma, si lo desea), ambos temporarios 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 asignación sea más o menos insensible, aunque sea perfectamente válida).No funciona con
f6
ya que devuelve unaconst X
a la que no puede asignar. Del mismo modo,f7(f5())
no funciona, ya quef5
crea un objeto temporal y no se vincula a las referencias de valores no constantesX&
(C ++ 11 introdujo las referencias de valoresX&&
para este propósito, pero esa es una historia diferente). Funcionaría sif7
tomara una referenciaconst X&
, ya que las referencias constantes de lvalue se unen a los temporales (pero luegof7
ya no funcionaría, por supuesto).
Aquí hay un ejemplo de lo que realmente sucede cuando ejecutas tu código. He hecho algunas modificaciones para aclarar los procesos detrás de la escena:
#include <iostream>
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 compiladores modernos se evitará la construcción de copias. Esto se debe a la optimización que se realiza (Optimización del valor de retorno) por el compilador. En mi salida, he eliminado explícitamente la optimización para mostrar lo que realmente sucede de acuerdo con el estándar. Si desea eliminar esta optimización también use la siguiente opción:
-fno-elide-constructors
No estaba completamente satisfecho con las respuestas, así que eché un vistazo a:
"Más efectivo C ++", Scott Meyers. Ítem 19: "Entender el origen de los objetos temporales"
. Con respecto a la cobertura de Bruce Eckel de "Temporaries", bueno, como sospecho y como lo señala directamente Christian Rau, ¡está mal! Grrr! Él está (de Eckel) usándonos como conejillos de indias! (Sería un buen libro para novatos como yo una vez que corrija todos sus errores)
Meyer: "Los verdaderos objetos temporales en C ++ son invisibles. No aparecen en su código fuente. Surgen cuando se crea un objeto que no es de pila, pero no tienen nombre. Dichos objetos sin nombre generalmente surgen en una de dos situaciones: cuando las conversiones de tipo son implícitas se aplican para que las llamadas a funciones tengan éxito y cuando las funciones devuelven objetos ".
"Considere primero el caso en el que los objetos temporales se crean para que las llamadas a las funciones tengan éxito. Esto sucede cuando el tipo de objeto que se pasa a una función no es el mismo que el tipo de parámetro al que se está enlazando".
"Estas conversiones se producen solo cuando se pasan objetos por valor o cuando se pasa a un parámetro de referencia a constante. No se producen cuando se pasa un objeto a un parámetro de referencia a no constante".
"El segundo conjunto de circunstancias en las que se crean objetos temporales es cuando una función devuelve un objeto".
"Cada vez que vea un parámetro de referencia a constante, existe la posibilidad de que se cree un temporal para enlazar con ese parámetro. En cualquier momento 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":
"se utiliza un constructor de copia 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 copia es definir lo que significa pasar y devolver objetos por valor".
Respecto a mis preguntas:
f5() = X(1) //what is happening?
Aquí no se está inicializando un nuevo objeto, pero esto no es una 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ó que utiliza un pseudocódigo, se convierte en: __0.operator=(__1)
y se realiza una copia a nivel de bits (realizada por el compilador).
Respecto a:
void f7(X& x);
f7(f5);
ergo, no se puede crear un temporal (Meyer: párrafos principales). Si hubiera sido declarado: void f7(const X& x);
entonces se habría creado un temporal.
Respecto a un objeto temporal que es una constante:
Meyer lo dice (y Matthieu): "se creará un temporal para enlazar con ese parámetro".
Por lo tanto, un temporal solo está vinculado a una referencia constante y, en sí mismo, no es un objeto "const".
Respecto a: ¿qué es X(1)
?
Meyer, artículo 27, efectivo C ++ - 3e, dice:
"Las conversiones de estilo C se parecen a esto: (T) expresión // expresión de conversión para ser de tipo T
Las conversiones de estilo de función utilizan esta sintaxis: T (expresión) // expresión de conversión para ser de tipo T "
Entonces X(1)
es un reparto de estilo de función. 1
la expresión se convierte al tipo X
Y Meyer lo dice de nuevo:
"Casi la única vez que uso una conversión de estilo 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<Widget>(15));
De alguna manera, la creación deliberada de objetos no se "siente" como un reparto, por lo que probablemente usaría el reparto de estilo de función en lugar del static_cast en este caso ".
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. (Porque Stroustrup sintió que podía provocar errores lógicos ...)
El único problema es que puede invocar un método en un temporal: por lo tanto, X(1).modify()
está bien, pero f7(X(1))
no lo está.
En cuanto a donde se crea el temporal, este es el trabajo del compilador. Las reglas del lenguaje precisan que lo temporal solo debe sobrevivir hasta el final de la expresión completa actual (y ya no), lo cual es importante para las instancias temporales de clases cuyo destructor tiene un efecto secundario.
Por lo tanto, la siguiente declaración X(1).modify();
se puede traducir completamente a:
{
X __0(1);
__0.modify();
} // automatic cleanup of __0
Con eso en mente, podemos atacar f5() = X(1);
. Tenemos dos temporarios aquí, y una asignación. Ambos argumentos de la asignación deben evaluarse completamente antes de llamar a 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 se inicializan __0
y __1
)
Y la clave para que funcione es que __0.operator=(__1)
es una invocación de método, y los métodos pueden invocarse en temporarios :)