Devolver unique_ptr de funciones

367

unique_ptr<T>no permite la construcción de copias, sino que admite la semántica de movimiento. Sin embargo, puedo devolver un unique_ptr<T>de 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 esa línea 1no invoca al constructor de la copia y produce errores de compilación? Si tuviera que usar la línea 2en su lugar, tendría sentido (usar la línea también 2funciona, pero no estamos obligados a hacerlo).

Sé que C ++ 0x permite esta excepción unique_ptrya 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, ¿se trata de un caso especial en el compilador o hay alguna otra cláusula en la especificación del lenguaje que explota?

Pretoriano
fuente
Hipotéticamente, si estuviera implementando un método de fábrica , ¿preferiría 1 o 2 para devolver el resultado 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 lo construido pase a la persona que llama.
Xharlie
77
@Xharlie? Ambos pasan la propiedad de la unique_ptr. La pregunta completa es que 1 y 2 son dos formas diferentes de lograr lo mismo.
Pretoriano
en este caso, el RVO también tiene lugar en c ++ 0x, la destrucción del objeto unique_ptr será una vez que se realice después de que la mainfunción salga, pero no cuando foosalga.
ampawd

Respuestas:

219

¿hay alguna otra cláusula en la especificación del lenguaje que explote?

Sí, ver 12.8 §34 y §35:

Cuando se cumplen ciertos criterios, se permite una implementación de omitir la construcción copiar / mover de un objeto de clase [...] Esta elisión de las operaciones de copiar / mover, llamada copia elisión , se permite [...] en un comunicado 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 cv-no calificado que 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 a copiar se designa mediante un valor l, la resolución de sobrecarga para seleccionar el constructor para la copia se realiza primero como si el objeto fuera designado por un valor r .


Solo quería agregar un punto más de que la devolución por valor debería ser la opción predeterminada aquí porque un valor con nombre en la declaración de devolución en el peor de los casos, es decir, sin elisiones en C ++ 11, C ++ 14 y C ++ 17 se trata como un valor Entonces, por ejemplo, la siguiente función se compila con la -fno-elide-constructorsbandera

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 configurado en la compilación, hay dos movimientos (1 y 2) que suceden en esta función y luego uno más adelante (3).

flujo libre
fuente
@juanchopanza ¿Quiere decir esencialmente que foo()también está a punto de ser destruido (si no se asignó 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 cuando lo hace unique_ptr<int> p = foo();?
7cows
1
Esta respuesta dice que una implementación puede hacer algo ... no dice que debe hacerlo, por lo que si esta fuera la única sección relevante, eso implicaría que confiar en este comportamiento no es portátil. Pero no creo que 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.
Don Hatch
66
@DonHatch Dice que está "permitido" realizar la elisión de copia / movimiento en esos casos, pero no estamos hablando de la elisión de copia aquí. Es el segundo párrafo citado que se aplica aquí, que se basa en las reglas de copia de elisión, pero no es la copia de la propia elisión. No hay incertidumbre en el segundo párrafo: es totalmente portátil.
Joseph Mansfield
@juanchopanza Me doy cuenta de que esto es ahora 2 años después, pero ¿todavía sientes que esto está mal? Como mencioné en el comentario anterior, no se trata de copiar elisión. Sucede que en los casos en los que se puede aplicar la elisión de copia (incluso si no se puede aplicar std::unique_ptr), existe una regla especial para tratar primero los objetos como valores. Creo que esto está completamente de acuerdo con lo que Nikola ha respondido.
Joseph Mansfield
1
Entonces, ¿por qué sigo recibiendo el error "intentando hacer referencia a una función eliminada" para mi tipo de solo movimiento (constructor de copia eliminado) cuando lo devuelvo exactamente de la misma manera que en este ejemplo?
DrumM
104

Esto no es específico de ninguna manera std::unique_ptr, pero se aplica a cualquier clase que sea móvil. Está garantizado por las reglas del idioma, ya que está regresando por valor. El compilador intenta eludir copias, invoca un constructor de movimientos si no puede eliminar copias, llama a un constructor de copias si no puede moverse y no puede compilar si no puede copiar.

Si tuviera una función que acepte std::unique_ptrcomo argumento, no podría pasarle p. Tendría que invocar explícitamente el constructor de movimiento, 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
    return 0;
}
Nikola Smiljanić
fuente
3
@Fred - bueno, en realidad no. Aunque pno 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 mainposible la asignación . Diría que te equivocaste, excepto que Nikola parece aplicar esta regla a psí misma, lo cual ES un error.
Edward Strange el
Exactamente lo que quería decir, pero no pude encontrar las palabras. Eliminé esa parte de la respuesta ya que no estaba muy clara.
Nikola Smiljanić
Tengo una pregunta: en la pregunta original, ¿hay alguna diferencia sustancial entre Line 1y Line 2? En mi opinión, es el mismo desde entonces cuando se construye pen main, sólo se preocupa por el tipo de tipo de retorno de foo, ¿verdad?
Hongxu Chen
1
@HongxuChen En ese ejemplo no hay absolutamente ninguna diferencia, vea la cita del estándar en la respuesta aceptada.
Nikola Smiljanić
En realidad, puede usar p después, siempre que lo asigne. Hasta entonces, no puede intentar hacer referencia a los contenidos.
Alan
38

unique_ptr no tiene el constructor de copia tradicional. En su lugar, tiene un "constructor de movimientos" que utiliza referencias rvalue:

unique_ptr::unique_ptr(unique_ptr && src);

Una referencia de valor r (el doble ampersand) solo se unirá a un valor r. Es por eso que obtiene 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 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 temporal unique_ptr aquí es un rvalue.

Bartosz Milewski
fuente
8
Creo que el punto es más, ¿por qué puede p, "obviamente" un valor l , ser tratado como un valor r 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í se puede "mover".
CB Bailey
¿Ajustar el valor devuelto de la función en std :: move significa que se moverá dos veces?
3
@RodrigoSalazar std :: move es solo un elenco sofisticado de una referencia de valor (&) a una referencia de valor (&&). El uso extraño de std :: move en una referencia de rvalue simplemente será un noop
TiMoch
13

Creo que está perfectamente explicado en el ítem 25 de Effective Modern C ++ 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 una elisión de copia, el objeto que se devuelve debe tratarse como un valor. En efecto, el Estándar requiere que cuando se permite el RVO, se realiza una elisión de copia o std::movese aplica implícitamente 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 esperaría hacer el RVO , que también se explica muy bien en el artículo 25 de su libro al referirse a el 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 que se realiza una elisión de copia o std::movese aplica implícitamente a los objetos locales que se devuelven . Scott menciona en el ítem 25 que std::movese 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 py el tipo de pes el mismo que el tipo de retorno, lo que da como resultado una elisión de copia. Y si el compilador elige no eludir la copia, por cualquier razón, std::movese hubiera puesto en línea 1.

David Lee
fuente
5

Una cosa que no vi en otras respuestas esPara aclarar otras respuestas, hay una diferencia entre devolver std :: unique_ptr que se ha creado dentro de una función y una 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));
v010dya
fuente
Se menciona en la respuesta de fredoverflow : " objeto automático " claramente resaltado . Una referencia (incluida una referencia rvalue) no es un objeto automático.
Toby Speight
@TobySpeight Ok, lo siento. Supongo que mi código es solo una aclaración entonces.
v010dya