c++ - ¿Por qué devolver un std:: opcional a veces se mueve y otras veces se copia?
c++17 return-value-optimization (3)
devuelve { arg1, arg2, ... };
es copy-list-initialization . el objeto (devuelto) se inicializa desde la lista de inicializadores mediante la inicialización de copia para la inicialización de lista de copia
Vea el siguiente ejemplo de devolución de un nombre de usuario opcional: una clase movible / copiable.
std::optional<UserName> CreateUser()
{
UserName u;
return {u}; // this one will cause a copy of UserName
return u; // this one moves UserName
}
int main()
{
auto d = CreateUser();
}
¿Por qué return {u}
causa una copia y te return u
un movimiento?
Aquí está la muestra de coliru relacionada: http://coliru.stacked-crooked.com/a/6bf853750b38d110
Otro caso (gracias al comentario de @Slava):
std::unique_ptr<int> foo()
{
std::unique_ptr<int> p;
return {p}; // uses copy of unique_ptr and so it breaks...
}
Debido a que devolver un nombre de un objeto con duración de almacenamiento automático se trata como devolver un valor de rvalor del objeto. Tenga en cuenta que esto solo funciona si la expresión en la declaración de retorno es un nombre (posiblemente entre paréntesis, sin incluir llaves), como return u;
o return (u);
, entonces return {u};
Funciona como siempre, es decir, se invoca el constructor de copia.
Parte relacionada en el estándar [class.copy.elision]/3 :
En los siguientes contextos de inicialización de copia, se puede utilizar una operación de movimiento en lugar de una operación de copia:
- Si la expresión en una declaración de retorno ([stmt.return]) es una expresión-id (posiblemente entre paréntesis) que nombra un objeto con una duración de almacenamiento automático declarada en el cuerpo o cláusula-declaración-parámetro de la función de cierre más interna o expresión-lambda o
- ...
La resolución de sobrecarga para seleccionar el constructor para la copia se realiza primero como si el objeto fuera designado por un valor de r.
Este es un tipo de lista iniciada . [dcl.init.list]/1.3
Para ser aún más específico, es una " expr-or-braced-init-list [dcl.init]/1
de una declaración de devolución " [stmt.return]/2
Una declaración de retorno con cualquier otro operando se usará solo en una función cuyo tipo de retorno no sea cv void; la declaración de retorno inicializa el resultado de glvalue o el resultado de prvalue de la llamada de función (explícita o implícita) mediante la inicialización de la copia desde el operando .
A partir de este punto, permítanme responder la respuesta de xskxzr que menciona [class.copy.elision]/3
En los siguientes contextos de inicialización de copia , se puede utilizar una operación de movimiento en lugar de una operación de copia:
- Si la expresión en una declaración de retorno ([stmt.return]) es una expresión-id (posiblemente entre paréntesis) que nombra un objeto con una duración de almacenamiento automático declarada en el cuerpo o cláusula-declaración-parámetro de la función de cierre más interna o expresión-lambda o
En palabras humanas normales, la razón por la que se llama a copiar en lugar de mover porque la lista u
init iniciada le llama que resultó ser lvalue.
Por lo tanto, es posible que desee saber si la lista u
init iniciada le llama rvalue ...
return {std::move(u)};
Bueno, u
se ha movido a un nuevo valor de UserName
y la copia del trabajo funciona inmediatamente después.
Así que esto toma un movimiento como en
return u;
wandbox.org/permlink/7u1cPc0TG9gqToZD
#include <iostream>
#include <optional>
struct UserName
{
int x;
UserName() : x(0) {};
UserName(const UserName& other) : x(other.x) { std::cout << "copy " << x << "/n"; };
UserName(UserName&& other) : x(other.x) { std::cout << "move " << x << "/n"; };
};
std::optional<UserName> CreateUser()
{
UserName u;
return u; // this one moves UserName
}
std::optional<UserName> CreateUser_listinit()
{
UserName u;
auto whatever{u};
return whatever;
}
std::optional<UserName> CreateUser_listinit_with_copy_elision()
{
UserName u;
return {u};
}
std::optional<UserName> CreateUser_move_listinit_with_copy_elision()
{
UserName u;
return {std::move(u)};
}
int main()
{
std::cout << "CreateUser() :/n";
[[maybe_unused]] auto d = CreateUser();
std::cout << "/nCreateUser_listinit() :/n";
[[maybe_unused]] auto e = CreateUser_listinit();
std::cout << "/nCreateUser_listinit_with_copy_elision() :/n";
[[maybe_unused]] auto f = CreateUser_listinit_with_copy_elision();
std::cout << "/nCreateUser_move_listinit_with_copy_elision() :/n";
[[maybe_unused]] auto g = CreateUser_move_listinit_with_copy_elision();
}
impresión
CreateUser() :
move 0
CreateUser_listinit() :
copy 0
move 0
CreateUser_listinit_with_copy_elision() :
copy 0
CreateUser_move_listinit_with_copy_elision() :
move 0