¿Qué nos dice auto &&?

168

Si lees código como

auto&& var = foo();

donde fooes cualquier función que regresa por valor de tipo T. Entonces varhay un lvalue de tipo rvalue referencia a T. ¿Pero para qué implica esto var? ¿Significa que se nos permite robar los recursos de var? ¿Hay situaciones razonables en las que debería usar auto&&para decirle al lector de su código algo como lo hace cuando devuelve un mensaje unique_ptr<>para decirle que tiene la propiedad exclusiva? ¿Y qué pasa, por ejemplo, T&&cuando Tes de tipo de clase?

Solo quiero entender, si hay otros casos de uso auto&&distintos a los de la programación de plantillas; como los discutidos en los ejemplos de este artículo Referencias universales de Scott Meyers.

MWid
fuente
1
Me he estado preguntando lo mismo. Entiendo cómo funciona la deducción de tipo, pero ¿qué dice mi código cuando lo uso auto&&? He estado pensando en por qué un bucle basado en rango se expande para usarlo auto&&como ejemplo, pero no he llegado a eso. Quizás quien responda pueda explicarlo.
Joseph Mansfield
1
¿Es esto legal? Me refiero a que el instinto de T se destruye inmediatamente después de los fooretornos, almacenar un valor de referencia a él suena como UB a ne.
1
@aleguna Esto es perfectamente legal. No quiero devolver una referencia o puntero a una variable local, sino un valor. La función de foola fuerza, por ejemplo, el siguiente aspecto: int foo(){return 1;}.
MWid
9
Las referencias de @aleguna a los temporales realizan una extensión de por vida, como en C ++ 98.
ecatmur
55
La extensión de por vida de @aleguna solo funciona con temporarios locales, no con funciones que devuelven referencias. Ver stackoverflow.com/a/2784304/567292
ecatmur el

Respuestas:

232

Al usarlo auto&& var = <initializer>estás diciendo: aceptaré cualquier inicializador independientemente de si es una expresión de valor o valor y preservaré su constidad . Esto se usa generalmente para reenviar (generalmente con T&&). La razón por la que esto funciona es porque una "referencia universal", auto&&o T&&, se unirá a cualquier cosa .

Podrías decir, bueno, ¿por qué no solo usar un const auto&porque eso también se unirá a cualquier cosa? El problema con el uso de una constreferencia es que es const! Posteriormente, no podrá vincularlo a ninguna referencia que no sea constante o invocar funciones miembro que no estén marcadas const.

Como ejemplo, imagine que desea obtener un std::vector, tome un iterador a su primer elemento y modifique el valor señalado por ese iterador de alguna manera:

auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;

Este código se compilará bien independientemente de la expresión inicializadora. Las alternativas a auto&&fallar de las siguientes maneras:

auto         => will copy the vector, but we wanted a reference
auto&        => will only bind to modifiable lvalues
const auto&  => will bind to anything but make it const, giving us const_iterator
const auto&& => will bind only to rvalues

Entonces, para esto, ¡ auto&&funciona perfectamente! Un ejemplo de uso auto&&como este es en un forbucle basado en rango . Vea mi otra pregunta para más detalles.

Si luego lo usa std::forwarden suauto&& referencia para preservar el hecho de que originalmente era un valor l o un valor r, su código dice: Ahora que obtuve su objeto de una expresión lvalue o rvalue, quiero preservar cualquier valor originalmente tenía para poder usarlo de manera más eficiente, esto podría invalidarlo. Como en:

auto&& var = some_expression_that_may_be_rvalue_or_lvalue;
// var was initialized with either an lvalue or rvalue, but var itself
// is an lvalue because named rvalues are lvalues
use_it_elsewhere(std::forward<decltype(var)>(var));

Esto permite use_it_elsewhereextraer sus agallas por el bien del rendimiento (evitando copias) cuando el inicializador original era un valor variable modificable.

¿Qué significa esto en cuanto a si podemos o cuándo podemos robar recursos var? Bueno, ya que auto&&se unirá a cualquier cosa, no podemos tratar de extraervar tripas nosotros mismos, puede muy bien ser un valor o incluso constante. Sin embargo, podemos std::forwardllevarlo a otras funciones que pueden devastar totalmente sus entrañas. Tan pronto como hagamos esto, deberíamos considerar varestar en un estado no válido.

Ahora apliquemos esto al caso de auto&& var = foo();, como se indica en su pregunta, donde foo devuelve un Tby por valor. En este caso, sabemos con certeza que el tipo de varse deducirá como T&&. Como sabemos con certeza que es un valor, no necesitamos std::forwardel permiso para robar sus recursos. En este caso específico, sabiendo que fooretorna por valor , el lector debería leerlo como: Estoy tomando una referencia de valor al temporal devuelto porfoo , por lo que felizmente puedo pasar de él.


Como un apéndice, creo que vale la pena mencionar cuándo some_expression_that_may_be_rvalue_or_lvaluepodría aparecer una expresión como , además de una situación de "bien, su código podría cambiar". Así que aquí hay un ejemplo artificial:

std::vector<int> global_vec{1, 2, 3, 4};

template <typename T>
T get_vector()
{
  return global_vec;
}

template <typename T>
void foo()
{
  auto&& vec = get_vector<T>();
  auto i = std::begin(vec);
  (*i)++;
  std::cout << vec[0] << std::endl;
}

Aquí get_vector<T>()está esa hermosa expresión que podría ser un valor o un valor según el tipo genérico T. Esencialmente cambiamos el tipo de retorno deget_vector través del parámetro de plantilla de foo.

Cuando llamamos foo<std::vector<int>>, get_vectorregresará global_vecpor valor, lo que da una expresión de valor. Alternativamente, cuando llamemos foo<std::vector<int>&>, get_vectorregresaremosglobal_vec por referencia, lo que resultado una expresión de valor.

Si lo hacemos:

foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;

Obtenemos el siguiente resultado, como se esperaba:

2
1
2
2

Si se va a cambiar el auto&&en el código a cualquiera de auto, auto&, const auto&, o const auto&&entonces no conseguimos el resultado que queremos.


Una forma alternativa de cambiar la lógica del programa en función de si su auto&&referencia se inicializa con una expresión lvalue o rvalue es usar rasgos de tipo:

if (std::is_lvalue_reference<decltype(var)>::value) {
  // var was initialised with an lvalue expression
} else if (std::is_rvalue_reference<decltype(var)>::value) {
  // var was initialised with an rvalue expression
}
Joseph Mansfield
fuente
2
¿No podemos simplemente decir T vec = get_vector<T>();dentro de la función foo? ¿O lo estoy simplificando a un nivel absurdo :)
Asterisk
@Asterisk No bcoz T vec solo se puede asignar a lvalue en el caso de std :: vector <int &> y si T es std :: vector <int> entonces usaremos call by value que es ineficiente
Kapil
1
auto y me da el mismo resultado. Estoy usando MSVC 2015. Y GCC produce un error.
Sergey Podobry
Aquí estoy usando MSVC 2015, auto & da los mismos resultados que el de auto &&.
Kehe CAI
¿Por qué es int i; auto && j = i; permitido pero int i; int && j = i; no es ?
SeventhSon84
14

Primero, recomiendo leer esta respuesta mía como una lectura paralela para una explicación paso a paso sobre cómo funciona la deducción de argumentos de plantilla para referencias universales.

¿Significa que se nos permite robar los recursos de var?

No necesariamente. ¿Qué sucede si foo()de repente devuelve una referencia o si cambió la llamada pero olvidó actualizar el uso de var? O si está en código genérico y el tipo de retorno defoo() puede cambiar según sus parámetros?

Piense auto&&que ser exactamente la misma que la T&&de template<class T> void f(T&& v);, porque es (casi ) exactamente eso. ¿Qué haces con las referencias universales en las funciones, cuando necesitas pasarlas o usarlas de alguna manera? Se utiliza std::forward<T>(v)para recuperar la categoría de valor original. Si era un valor de l antes de pasar a su función, permanece como un valor de l después de pasarlo std::forward. Si fue un valor r, se convertirá en un valor r nuevamente (recuerde, una referencia de valor r nombrada es un valor l).

Entonces, ¿cómo se usa varcorrectamente de manera genérica? Uso std::forward<decltype(var)>(var). Esto funcionará exactamente igual que std::forward<T>(v)en la plantilla de función anterior. Si vares un T&&, obtendrá un valor de retorno, y si es así T&, obtendrá un valor de retorno.

Entonces, volviendo al tema: ¿Qué nos dicen auto&& v = f();y std::forward<decltype(v)>(v)en una base de código? Nos dicen que vserá adquirido y transmitido de la manera más eficiente. Sin embargo, recuerde que después de haber reenviado una variable de este tipo, es posible que se haya movido de ella, por lo que sería incorrecto usarla aún más sin reiniciarla.

Personalmente, lo uso auto&&en código genérico cuando necesito una variable modificable . El reenvío perfecto de un valor r está modificando, ya que la operación de movimiento potencialmente le roba sus entrañas. Si solo quiero ser vago (es decir, no deletrear el nombre del tipo, incluso si lo sé) y no necesito modificar (por ejemplo, cuando solo imprimo elementos de un rango), me atendré auto const&.


autoes diferente en la medida en que se auto v = {1,2,3};va a hacer vuna std::initializer_list, mientras que f({1,2,3})será un fracaso deducción.

Xeo
fuente
En la primera parte de su respuesta: quiero decir que si foo()devuelve un tipo de valor T, entonces var(esta expresión) será un valor l y su tipo (de esta expresión) será una referencia de valor r T(es decir T&&).
MWid el
@MWid: Tiene sentido, eliminó la primera parte.
Xeo
3

Considere algún tipo Tque tenga un constructor de movimiento, y suponga

T t( foo() );

usa ese constructor de movimiento.

Ahora, usemos una referencia intermedia para capturar el retorno de foo:

auto const &ref = foo();

esto descarta el uso del constructor de movimiento, por lo que el valor de retorno tendrá que copiarse en lugar de moverse (incluso si lo usamos std::moveaquí, en realidad no podemos movernos a través de una referencia constante)

T t(std::move(ref));   // invokes T::T(T const&)

Sin embargo, si usamos

auto &&rvref = foo();
// ...
T t(std::move(rvref)); // invokes T::T(T &&)

el constructor de movimiento todavía está disponible.


Y para abordar sus otras preguntas:

... ¿Hay situaciones razonables en las que debería usar auto && para decirle algo al lector de su código ...

Lo primero, como dice Xeo, es esencialmente que estoy pasando X de la manera más eficiente posible , sea cual sea el tipo X. Entonces, ver el código que usa auto&&internamente debería comunicar que usará la semántica de movimiento internamente cuando sea apropiado.

... como cuando devuelve un unique_ptr <> para indicar que tiene una propiedad exclusiva ...

Cuando una plantilla de función toma un argumento de tipo T&&, dice que puede mover el objeto que pasa. Al regresar unique_ptrexplícitamente se le otorga la propiedad al llamante; aceptar T&&puede eliminar la propiedad de la persona que llama (si existe un movimiento, etc.).

Inútil
fuente
2
No estoy seguro de que su segundo ejemplo sea válido. ¿No necesitas un reenvío perfecto para invocar al constructor de movimientos?
3
Esto está mal. En ambos casos, el constructor de copia se llama, ya que refy rvrefson ambos lvalues. Si quieres el constructor de movimiento, entonces tienes que escribir T t(std::move(rvref)).
MWid
¿Querías decir const ref en tu primer ejemplo auto const &:?
PiotrNycz
@aleguna: usted y MWid tienen razón, gracias. He arreglado mi respuesta.
Inútil
1
@ Inútil Tienes razón. Pero esto no responde a mi pregunta. ¿Cuándo usas auto&&y qué le dices al lector de tu código usando auto&&?
MWid el
-3

La auto &&sintaxis utiliza dos nuevas características de C ++ 11:

  1. La autoparte permite que el compilador deduzca el tipo según el contexto (el valor de retorno en este caso). Esto es sin ninguna calificación de referencia (lo que le permite especificar si lo desea T, T &o T &&para un tipo deducido T).

  2. El &&es el nuevo movimiento semántico. Un tipo que admite semántica de movimiento implementa un constructor T(T && other)que mueve de manera óptima el contenido en el nuevo tipo. Esto permite que un objeto intercambie la representación interna en lugar de realizar una copia profunda.

Esto le permite tener algo como:

std::vector<std::string> foo();

Entonces:

auto var = foo();

realizará una copia del vector devuelto (caro), pero:

auto &&var = foo();

intercambiará la representación interna del vector (el vector de fooy el vector vacío de var), por lo que será más rápido.

Esto se usa en la nueva sintaxis for-loop:

for (auto &item : foo())
    std::cout << item << std::endl;

Donde el ciclo for está reteniendo un auto &&valor de retorno fooy itemes una referencia a cada valor en foo.

reece
fuente
Esto es incorrecto. auto&&no moverá nada, solo hará una referencia. Si se trata de una referencia lvalue o rvalue depende de la expresión utilizada para inicializarla.
Joseph Mansfield
En ambos casos se consideró que la decisión del constructor, ya que std::vectory std::stringson moveconstructible. Esto no tiene nada que ver con el tipo de var.
MWid el
1
@MWid: En realidad, la llamada al constructor de copiar / mover también se puede eludir por completo con RVO.
Matthieu M.
@MatthieuM. Tienes razón. Pero creo que en el ejemplo anterior, nunca se llamará al cnstructor de copia, ya que todo es moveconstructible.
MWid
1
@MWid: mi punto era que incluso el constructor de movimiento se puede eludir. Elision triunfa sobre el movimiento (es más barato).
Matthieu M.