c++ - make_unique - Devolviendo unique_ptr desde funciones
c++11 unique-ptr (5)
unique_ptr<T>
no permite la construcción de copias, en su lugar admite movimientos semánticos. Sin embargo, puedo devolver un unique_ptr<T>
desde una función y asignar el valor devuelto a una variable.
#include <iostream>
#include <memory>
using namespace std;
unique_ptr<int> foo()
{
unique_ptr<int> p( new int(10) );
return p; // 1
//return move( p ); // 2
}
int main()
{
unique_ptr<int> p = foo();
cout << *p << endl;
return 0;
}
El código anterior compila y funciona según lo previsto. Entonces, ¿cómo es que la línea 1
no invoca al constructor de copia y da como resultado errores del compilador? Si tuviera que usar la línea 2
en su lugar, tendría sentido (usar la línea 2
funciona, pero no estamos obligados a hacerlo).
Sé que C ++ 0x permite esta excepción a unique_ptr
ya que el valor de retorno es un objeto temporal que se destruirá tan pronto como la función salga, garantizando así la unicidad del puntero devuelto. Tengo curiosidad acerca de cómo se implementa esto, ¿es especial en el compilador o hay alguna otra cláusula en la especificación de lenguaje que explota?
¿Hay alguna otra cláusula en la especificación de lenguaje que explota?
Sí, ver 12.8 §34 y §35:
Cuando se cumplen ciertos criterios, una implementación puede omitir la construcción de copiar / mover de un objeto de clase [...] Esta elisión de operaciones de copia / mover, llamada copia de elision , está permitida [...] en una declaración de retorno en una función con un tipo de retorno de clase, cuando la expresión es el nombre de un objeto automático no volátil con el mismo tipo no cualificado cv que el tipo de retorno de función [...]
Cuando se cumplen los criterios para la elección de una operación de copia y el objeto que se va a copiar se designa con un valor l, la resolución de sobrecarga para seleccionar el constructor para la copia se realiza primero como si el objeto estuviera designado con un valor r .
Solo quería agregar un punto más que devolver por valor debería ser la opción predeterminada aquí porque se trata un valor con nombre en la declaración de retorno en el peor de los casos, es decir, sin elecciones en C ++ 11, C ++ 14 y C ++ 17 se tratan como un valor. Así, por ejemplo, la siguiente función se compila con el -fno-elide-constructors
std::unique_ptr<int> get_unique() {
auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
return ptr; // <- 2, moved into the to be returned unique_ptr
}
...
auto int_uptr = get_unique(); // <- 3
Con la bandera activada en la compilación, hay dos movimientos (1 y 2) ocurriendo en esta función y luego un movimiento más adelante (3).
Creo que está perfectamente explicado en el artículo 25 de la C ++ moderna efectiva de Scott Meyers. Aquí hay un extracto:
La parte de la bendición estándar del RVO continúa diciendo que si se cumplen las condiciones para el RVO, pero los compiladores eligen no realizar copias de la copia, el objeto que se devuelve debe tratarse como un valor de r. En efecto, el Estándar requiere que cuando se permita el RVO, se realice una copia de la copia o se aplique implícitamente
std::move
a los objetos locales que se devuelven.
Aquí, RVO se refiere a la optimización del valor de retorno , y si se cumplen las condiciones para el RVO significa devolver el objeto local declarado dentro de la función que se esperaría que hiciera el RVO , que también se explica muy bien en el artículo 25 de su libro refiriéndose a el estándar (aquí el objeto local incluye los objetos temporales creados por la declaración de retorno). El mayor alejamiento del extracto es que se realiza una copia de la copia o std::move
se aplica implícitamente a los objetos locales que se devuelven . Scott menciona en el elemento 25 que std::move
se aplica implícitamente cuando el compilador elige no ocultar la copia y el programador no debe hacerlo explícitamente.
En su caso, el código es claramente un candidato para RVO, ya que devuelve el objeto local p
y el tipo de p
es el mismo que el tipo de retorno, lo que da como resultado una elección de copia. Y si el compilador decide no ocultar la copia, por el motivo que sea, std::move
habría pateado a la línea 1
.
Esto no es de ninguna manera específico para std::unique_ptr
, pero se aplica a cualquier clase que sea movible. Está garantizado por las reglas de idioma, ya que está devolviendo por valor. El compilador intenta ocultar copias, invoca un constructor de movimiento si no puede eliminar copias, llama a un constructor de copia si no puede moverse y no puede compilar si no puede copiar.
Si tuviera una función que acepte std::unique_ptr
como argumento, no podría pasarle p. Tendría que invocar explícitamente Move constructor, pero en este caso no debería usar la variable p después de la llamada a la bar()
.
void bar(std::unique_ptr<int> p)
{
// ...
}
int main()
{
unique_ptr<int> p = foo();
bar(p); // error, can''t implicitly invoke move constructor on lvalue
bar(std::move(p)); // OK but don''t use p afterwards
return 0;
}
unique_ptr no tiene el constructor de copia tradicional. En su lugar, tiene un "constructor de movimiento" que utiliza referencias de valor de r:
unique_ptr::unique_ptr(unique_ptr && src);
Una referencia rvalue (el doble ampersand) solo se unirá a un rvalue. Es por eso que recibe un error cuando intenta pasar un lvalue unique_ptr a una función. Por otro lado, un valor que se devuelve desde una función se trata como un valor de r, por lo que el constructor de movimiento se llama automáticamente.
Por cierto, esto funcionará correctamente:
bar(unique_ptr<int>(new int(44));
El unique_ptr temporal aquí es un rvalue.
Una cosa que no vi en otras respuestas es Para aclarar share hay una diferencia entre devolver std :: unique_ptr que se ha creado dentro de una función y uno que se le ha dado a esa función.
El ejemplo podría ser así:
class Test
{int i;};
std::unique_ptr<Test> foo1()
{
std::unique_ptr<Test> res(new Test);
return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
// return t; // this will produce an error!
return std::move(t);
}
//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));