c++ - ¿Qué significa auto && decirnos?
c++11 universal-reference (4)
Si lees código como
auto&& var = foo();
donde foo es cualquier función que resintoniza por valor de tipo T
Entonces var
es un valor l de referencia de tipo rvalue a T
Pero, ¿qué implica esto para var
? ¿Significa que estamos autorizados a robar los recursos de var
? ¿Hay situaciones razonables en las que deba usar auto&&
para decirle al lector de su código algo como lo hace cuando devuelve un unique_ptr<>
para decirle que tiene propiedad exclusiva? ¿Y qué pasa con, por ejemplo, T&&
cuando T
es del tipo de clase?
Solo quiero entender, si hay otros casos de uso de auto&&
que aquellos en la programación de plantillas como se discutió en los ejemplos de este artículo Referencias universales de Scott Meyers.
Al usar auto&& var = <initializer>
está diciendo: Aceptaré cualquier inicializador independientemente de si es una expresión lvalue o rvalue y preservaré su constness . Esto normalmente se usa para reenviar (generalmente con T&&
). La razón por la que esto funciona es porque una "referencia universal", auto&&
o T&&
, se vinculará a cualquier cosa .
Podrías decir, ¿por qué no usar un const auto&
porque eso también se unirá a algo? El problema con el uso de una referencia const
es que es const
! No podrá enlazarlo más tarde a ninguna referencia que no sea const ni invocar ninguna función miembro que no esté marcada como const
.
Como ejemplo, imagina que quieres obtener un std::vector
, lleva un iterador a su primer elemento y modifica el valor apuntado 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 compilará muy bien independientemente de la expresión del inicializador. Las alternativas a auto&&
fallan 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
¡Así que para esto, auto&&
funciona perfectamente! Un ejemplo de usar auto&&
como este está en un bucle for
basado en rango. Ver mi otra pregunta para más detalles.
Si luego usa std::forward
en su auto&&
reference para preservar el hecho de que originalmente era un valor lval o un valor r, su código dice: Ahora que tengo su objeto de una expresión lvalue o rvalue, quiero preservar el valor que originalmente tenía para poder usarlo de la 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 a use_it_elsewhere
extraer sus agallas por el bien de la ejecución (evitando copias) cuando el inicializador original era un valor r modificable.
¿Qué significa esto si podemos o cuándo podemos robar recursos de var
? Bueno, dado que el auto&&
se unirá a cualquier cosa, no podemos posiblemente intentar arrancarnos las tripas - puede muy bien ser un lvalue o incluso const. Sin embargo, podemos std::forward
a otras funciones que pueden dañar totalmente sus entrañas. Tan pronto como hagamos esto, deberíamos considerar que var
está en un estado inválido.
Ahora apliquemos esto al caso de auto&& var = foo();
, como se indica en su pregunta, donde foo devuelve una T
por valor. En este caso, sabemos con certeza que el tipo de var
se deducirá como T&&
. Como sabemos con certeza que se trata de un valor r, no necesitamos el permiso de std::forward
para robar sus recursos. En este caso específico, sabiendo que foo
regresa por valor , el lector debería leerlo como: estoy tomando una referencia rvalue del temporal devuelto por foo
, así que puedo moverme felizmente de él.
Como apéndice, creo que vale la pena mencionar cuándo podría some_expression_that_may_be_rvalue_or_lvalue
una expresión como some_expression_that_may_be_rvalue_or_lvalue
, que no sea 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>()
es esa hermosa expresión que podría ser un lvalue o rvalue dependiendo del tipo genérico T
Básicamente, cambiamos el tipo de get_vector
de get_vector
través del parámetro de plantilla de foo
.
Cuando llamamos a foo<std::vector<int>>
, get_vector
devolverá global_vec
por valor, lo que le da una expresión rvalue. Alternativamente, cuando llamemos a foo<std::vector<int>&>
, get_vector
devolverá global_vec
por referencia, lo que global_vec
resultado una expresión lvalue.
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 tuviera que cambiar el auto&&
en el código a auto
, auto&
, const auto&
, o const auto&&
entonces no obtendremos el resultado que queremos.
Una forma alternativa de cambiar la lógica del programa en función de si su auto&&
reference 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
}
Considere algún tipo T
que tenga un constructor de movimiento, y asuma
T t( foo() );
usa ese constructor de movimiento.
Ahora, usemos una referencia intermedia para capturar el retorno de foo
:
auto const &ref = foo();
esto excluye el uso del constructor de movimiento, por lo que el valor de retorno deberá ser copiado en lugar de movido (incluso si usamos std::move
aquí, 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 alguna situación razonable en la que deba usar auto && para decirle al lector algo sobre su código ...
Lo primero, como dice Xeo, es esencialmente que estoy pasando X de la manera más eficiente posible , cualquiera que sea el tipo X. Por lo tanto, ver el código que usa auto&&
internamente debe comunicar que usará la semántica de movimiento internamente cuando corresponda.
... como lo haces cuando devuelves un unique_ptr <> para decirte que tienes propiedad exclusiva ...
Cuando una plantilla de función toma un argumento de tipo T&&
, está diciendo que puede mover el objeto por el que pasa. La devolución de unique_ptr
otorga explícitamente la propiedad al llamador; aceptar T&&
puede eliminar la propiedad de la persona que llama (si existe un movimiento, etc.).
En primer lugar, recomiendo leer esta respuesta mía como una lectura paralela para obtener una explicación paso a paso sobre cómo funciona la deducción de argumentos de plantilla para referencias universales.
¿Significa que estamos autorizados a robar los recursos de
var
?
No necesariamente. ¿Qué pasa si foo()
de repente devolvió una referencia, o cambió la llamada pero olvidó actualizar el uso de var
? ¿O si está en un código genérico y el tipo de retorno de foo()
podría cambiar según sus parámetros?
Piense que auto&&
es exactamente igual que T&&
en la template<class T> void f(T&& v);
, porque es (casi † ) exactamente eso. ¿Qué hace con referencias universales en funciones, cuando necesita pasarlas o usarlas de alguna manera? Utiliza std::forward<T>(v)
para recuperar la categoría de valor original. Si se trata de un lvalue antes de pasarse a su función, permanece un lvalue después de pasar por std::forward
. Si se tratara de un valor r, se volverá a convertir en valor real (recuerde, una referencia de valor r es un valor l).
Entonces, ¿cómo se usa var
correctamente de forma genérica? Use std::forward<decltype(var)>(var)
. Esto funcionará exactamente igual que std::forward<T>(v)
en la plantilla de función de arriba. Si var
es un T&&
, obtendrás un valor de retorno, y si es T&
, obtendrás un valor de retorno.
Entonces, volviendo al tema: ¿Qué hacen auto&& v = f();
y std::forward<decltype(v)>(v)
en una base de código, díganos? Nos dicen que v
se adquirirá y se transmitirá de la manera más eficiente. Sin embargo, recuerde que después de haber reenviado dicha variable, es posible que se haya movido, por lo que sería incorrecto utilizarla aún más sin restablecerla.
Personalmente, uso auto&&
en código genérico cuando necesito una variable modificable . El reenvío perfecto de un valor r se está modificando, ya que la operación de movimiento puede robarle sus agallas. Si solo quiero ser flojo (es decir, no deletrear el nombre del tipo, incluso si lo sé) y no necesito modificarlo (por ejemplo, cuando solo estoy imprimiendo elementos de un rango), me atengo a auto const&
.
† auto
es tan diferente que auto v = {1,2,3};
creará v
un std::initializer_list
, mientras que f({1,2,3})
será un error de deducción.
La sintaxis auto &&
usa dos nuevas características de C ++ 11:
La parte
auto
permite al compilador deducir el tipo en función del contexto (el valor de retorno en este caso). Esto es sin ninguna calificación de referencia (lo que le permite especificar si deseaT
,T &
T &&
para un tipo deducidoT
).El
&&
es la nueva semántica de movimiento. Una semántica de movimiento de soporte de tipo implementa un constructorT(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();
Asi que:
auto var = foo();
realizará una copia del vector devuelto (costoso), pero:
auto &&var = foo();
intercambiará la representación interna del vector (el vector de foo
y 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 for-loop mantiene un auto &&
al valor de retorno de foo
y el item
es una referencia a cada valor en foo
.