2010-11-30 4 views
232

unique_ptr<T> no permite la construcción de copias, en cambio admite la semántica de movimientos. Sin embargo, puedo devolver un unique_ptr<T> desde una función y asignar el valor devuelto a una variable.La devolución de unique_ptr de las funciones

#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 el constructor de copia y da como resultado errores de compilación? Si tuviera que usar la línea 2, tendría sentido (usar la línea 2 también 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 singularidad del puntero devuelto. Tengo curiosidad acerca de cómo se implementa esto, ¿está incluido en el compilador especial o hay alguna otra cláusula en la especificación del lenguaje que explote?

+0

Hipotéticamente, si estuviera implementando un método * factory *, ¿preferiría que 1 o 2 devolvieran la producción de la fábrica? Supongo que este sería el uso más común de 1 porque, con una fábrica adecuada, realmente desea que la propiedad de la cosa construida pase a la persona que llama. – Xharlie

+5

@Xharlie? Ambos pasan la propiedad de 'unique_ptr'.Toda la cuestión es que 1 y 2 son dos formas diferentes de lograr lo mismo. – Praetorian

Respuesta

155

hay alguna otra cláusula en la especificación del lenguaje que este explota?

Sí, ver 12.8 §34 y §35:

Cuando se cumplen ciertos criterios, se permite que una aplicación para omitir la construcción copiar/mover de un objeto de clase [...] Este elisión de las operaciones de copiar/mover, llamado copia elisión, se permite [...] en una instrucción de retorno de una función con un tipo de retorno de clase, cuando la expresión es el nombre de un objeto automática no volátil con el mismo tipo cv no calificado como el tipo de retorno de función [...]

Cuando se cumplen los criterios para la elisión de una operación de copia y el objeto que se va a copiar se designa por un lvalue, resolución de sobrecarga para seleccionar el constructor para la copia se realiza primero como si el objeto se designaron por un rvalue.


sólo quería añadir un punto más que el retorno por valor debería ser la opción por defecto aquí porque un valor indicada en la sentencia de retorno en el peor de los casos, es decir, sin elisiones en C++ 11, C + +14 y C++ 17 se tratan como un valor r. Así, por ejemplo, la función siguiente se compila con la bandera -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 el indicador en la compilación hay dos movimientos (1 y 2) pasando en esta función y luego un movimiento más adelante (3).

+0

heh, estaba a punto de publicar una suposición diciendo lo mismo, pero no tenía el nuevo estándar en frente de mí. –

+0

@juanchopanza ¿Quiere decir esencialmente que 'foo()' también está a punto de ser destruido (si no está asignado a nada), al igual que el valor de retorno dentro de la función, y por lo tanto tiene sentido que C++ use un constructor de movimiento al hacer 'unique_ptr p = foo();'? – 7cows

+0

Esta respuesta dice que una implementación * está permitida * para hacer algo ... no dice que debe hacerlo, por lo que si esta fuera la única sección relevante, eso implicaría confiar en que este comportamiento no es portátil. Pero no creo que eso sea correcto. Me inclino a pensar que la respuesta correcta tiene más que ver con el constructor de movimientos, como se describe en la respuesta de Nikola Smiljanic y Bartosz Milewski. –

74

Esto no es de ninguna manera específico para unique_ptr, pero se aplica a cualquier clase que sea movible. Está garantizado por las reglas de idioma ya que regresas por valor. El compilador intenta eludir copias, invoca un constructor de movimiento si no puede eliminar copias, llama a un constructor de copia si no se puede mover y no puede compilar si no puede copiar.

Si tiene una función que acepta unique_ptr como argumento, no podrá pasarle p. Debería invocar explícitamente el constructor de movimientos, pero en este caso no debería usar la variable p después de la llamada a 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 
} 
+2

@Fred - bueno, en realidad no. Aunque 'p' no es temporal, el resultado de' foo() ', lo que se devuelve es; por lo tanto, es un valor r y se puede mover, lo que hace posible la asignación en 'main'. Yo diría que estabas equivocado, excepto que Nikola parece aplicar esta regla a 'p', que ES erróneo. –

+0

Exactamente lo que quería decir, pero no pude encontrar las palabras. Eliminé esa parte de la respuesta porque no estaba muy clara. –

+0

Tengo una pregunta: en la pregunta original, ¿hay alguna diferencia sustancial entre la Línea '1' y la Línea' 2'? En mi opinión, es lo mismo ya que al construir 'p' en' main', solo le importa el tipo de tipo de retorno 'foo', ¿verdad? –

32

unique_ptr no tiene el constructor de copia tradicional. En cambio, tiene un "movimiento constructor" que utiliza referencias rvalue:

unique_ptr::unique_ptr(unique_ptr && src); 

Una referencia rvalue (el doble ampersand) solamente se unirá a una rvalue. Es por eso que obtienes un error cuando tratas de pasar un lvalue unique_ptr a una función. Por otro lado, un valor que se devuelve de una función se trata como un valor r, por lo que el constructor de movimiento se llama automáticamente.

Por cierto, esto funciona correctamente:

bar(unique_ptr<int>(new int(44)); 

El unique_ptr temporal aquí es un valor de lado derecho.

+8

Creo que el punto es más, ¿por qué 'p' -" obviamente "un _valor_ - puede tratarse como un _valor_ en la declaración return' return p; 'en la definición de' foo'. No creo que haya ningún problema con el hecho de que el valor de retorno de la función en sí pueda "moverse". –

+0

¿Ajustar el valor devuelto de la función en std :: move significa que se moverá dos veces? –

+3

@RodrigoSalazar std :: move es solo un molde de fantasía de una referencia de lvalue (&) a una referencia de valor de referencia (&&). El uso extraño de std :: move en una referencia rvalue simplemente será un noop – TiMoch

1

Una cosa que no me veo en otras respuestas es Para aclarar another answers que hay una diferencia entre volver std :: unique_ptr que se ha creado dentro de una función, y uno que se 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)); 
+0

Se menciona en [respuesta por fredoverflow] (/ a/4316948) - claramente resaltado como "** objeto ** automático". Una referencia (que incluye una referencia rvalue) no es un objeto automático. –

+0

@TobySpeight Ok, lo siento. Supongo que mi código es solo una aclaración entonces. – v010dya

1

Creo que es perfectamente explicado en el punto 25 de Scott Meyers' Effective Modern C++. He aquí un extracto:

La parte de la Norma bendiciendo el RVO va a decir que si se cumplen las condiciones para la RVO, pero los compiladores elegir no realizar la elisión copia, siendo devuelto debe ser tratado como un objeto rvalue En efecto, el Estándar requiere que cuando se permita el RVO, se lleve a cabo una elisión de copia o std::move se aplique implícitamente a los objetos locales que se devuelvan.

Aquí, RVO se refiere a retorno optimización valor y si las condiciones para la RVO se cumplen medios de retorno del objeto local declarada dentro de la función que se puede esperar para hacer el RVO, que también se explica muy bien en el ítem 25 de su libro al referirse al estándar (aquí el objeto local incluye los objetos temporales creados por la declaración de devolución). La mayor extracción del extracto es , ya sea que se realice una elisión de copia o std::move en objetos locales que se devuelven. Scott menciona en el ítem 25 que std::move se aplica implícitamente cuando el compilador elige no eludir 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 resulta en la copia elisión. Y si el compilador decide no eludir la copia, por cualquier razón, std::move hubiera pateado la línea 1.

Cuestiones relacionadas