programacion - operador de resolucion de ambito c++
¿Se llama movimiento al constructor dos veces en C++? (2)
Mira este código:
class Foo
{
public:
string name;
Foo(string n) : name{n}
{
cout << "CTOR (" << name << ")" << endl;
}
Foo(Foo&& moved)
{
cout << "MOVE CTOR (moving " << moved.name << " into -> " << name << ")" << endl;
name = moved.name + " ###";
}
~Foo()
{
cout << "DTOR of " << name << endl;
}
};
Foo f()
{
return Foo("Hello");
}
int main()
{
Foo myObject = f();
cout << endl << endl;
cout << "NOW myObject IS EQUAL TO: " << myObject.name;
cout << endl << endl;
return 0;
}
La salida es:
[1] CTOR (Hola)
[2] MOVE CTOR (moviendo Hello a ->)
[3] DTOR de Hola
[4] MOVE CTOR (moviendo Hello ### a ->)
[5] DTOR de Hola ###
[6] AHORA dos es igual a: Hola ### ###
[7] DTOR de Hola ### ###
Nota importante: He deshabilitado la optimización de copia de elision usando -fno-elide-constructors
para propósitos de prueba.
La función f () construye un temporal [1] y lo devuelve llamando al constructor de movimiento para "mover" los recursos de ese temporal a myObject [2] (además, agrega 3 símbolos #).
Finalmente, lo temporal se destruye [3] .
Ahora espero que miObjeto esté completamente construido y que su atributo de nombre sea Hola ### .
En su lugar, el constructor de movimientos se llama OTRA VEZ, así que me quedo con Hola ### ###
Debido a que desactivó la elección de copia, su objeto primero se crea en f()
, luego se mueve al marcador de posición de valor de retorno para f()
. En este punto se destruye la copia local de f
. A continuación, el objeto de retorno se mueve a myObject
y también se destruye. Finalmente myObject
es destruido.
Si no desactivó la opción de copia, habría visto la secuencia que esperaba.
ACTUALIZACIÓN : para abordar la pregunta en el comentario a continuación, que es - dada la definición de una función como esta:
Foo f()
{
Foo localObject("Hello");
return localObject;
}
¿Por qué se invoca el constructor de movimientos en la creación del objeto de valor de retorno con elección de copia deshabilitada? Después de todo, localObject anterior es un lvalue.
La respuesta es que el compilador está obligado en estas circunstancias a tratar el objeto local como un valor, por lo que efectivamente está generando implícitamente el código return std::move(localObject)
. La regla que lo requiere es el estándar [class.copy / 32] (partes relevantes resaltadas):
Cuando se cumplen los criterios para la elección de una operación de copia / movimiento , pero no para una declaración de excepción, y el objeto que se va a copiar se designa con un lvalue, o cuando la expresión en una declaración de retorno es un id (posiblemente entre paréntesis) La expresión que nombra un objeto con una duración de almacenamiento automático declarada en el cuerpo o la cláusula de declaración de parámetros de la función de cierre más interna o la expresión lambda, la resolución de sobrecarga para seleccionar el constructor para la copia se realiza primero como si el objeto fuera designado por un valor .
...
[Nota: esta resolución de sobrecarga de dos etapas se debe realizar independientemente de si se producirá elision de copia . Determina el constructor que se llamará si no se realiza el elision, y el constructor seleccionado debe ser accesible incluso si la llamada es eluida. - nota final]
Las dos llamadas de constructor de movimiento son:
- Mueva el temporal creado por
Foo("Hello")
al valor de retorno. - Mueva el temporal devuelto por la llamada
f()
amyObject
.
Si utilizara una lista de iniciación con arriostrado para construir el valor de retorno, solo habría una construcción de movimiento único:
Foo f()
{
return {"Hello"};
}
Esto produce:
CTOR (Hello)
MOVE CTOR (moving Hello into -> )
DTOR of Hello
NOW myObject IS EQUAL TO: Hello ###
DTOR of Hello ###