¿Qué significa T&& (doble signo) en C++ 11?
He estado investigando algunas de las nuevas características de C++ 11 y una que he notado es el doble signo al declarar variables, como T&& var
.
Para empezar, ¿cómo se llama esta bestia? Ojalá Google nos permitiera buscar signos de puntuación como este.
¿ Qué significa exactamente ?
A primera vista, parece ser una referencia doble (como los punteros dobles estilo C T** var
), pero me cuesta pensar en un caso de uso para eso.
Declara una referencia de valor (documento de propuesta de estándares).
Aquí hay una introducción a las referencias de rvalue .
Aquí hay una fantástica mirada en profundidad a las referencias de rvalue realizada por uno de los desarrolladores de bibliotecas estándar de Microsoft .
PRECAUCIÓN: el artículo vinculado en MSDN ("Referencias de Rvalue: Características de C++0x en VC10, Parte 2") es una introducción muy clara a las referencias de Rvalue, pero hace declaraciones sobre las referencias de Rvalue que alguna vez fueron ciertas en el borrador de C++11. estándar, ¡pero no son válidos para el final! Específicamente, dice en varios puntos que las referencias de rvalue pueden vincularse a lvalues, lo que alguna vez fue cierto, pero se cambió (por ejemplo, int x; int &&rrx = x; ya no se compila en GCC).
La mayor diferencia entre una referencia de C++ 03 (ahora llamada referencia de valor l en C++ 11) es que puede vincularse a un valor r como temporal sin tener que ser constante. Por tanto, esta sintaxis ahora es legal:
T&& r = T();
Las referencias de rvalue proporcionan principalmente lo siguiente:
Mover semántica . Ahora se puede definir un constructor de movimiento y un operador de asignación de movimiento que tome una referencia de valor r en lugar de la referencia de valor constante habitual. Una mudanza funciona como una copia, excepto que no está obligada a mantener la fuente sin cambios; de hecho, normalmente modifica la fuente de modo que ya no posee los recursos movidos. Esto es excelente para eliminar copias superfluas, especialmente en implementaciones de bibliotecas estándar.
Por ejemplo, un constructor de copias podría verse así:
foo(foo const& other)
{
this->length = other.length;
this->ptr = new int[other.length];
copy(other.ptr, other.ptr + other.length, this->ptr);
}
Si a este constructor se le pasara un temporal, la copia sería innecesaria porque sabemos que el temporal simplemente será destruido; ¿Por qué no aprovechar los recursos que el temporal ya le ha asignado? En C++03, no hay forma de evitar la copia ya que no podemos determinar si se nos pasó un archivo temporal. En C++11, podemos sobrecargar un constructor de movimientos:
foo(foo&& other)
{
this->length = other.length;
this->ptr = other.ptr;
other.length = 0;
other.ptr = nullptr;
}
Observe la gran diferencia aquí: el constructor de movimientos en realidad modifica su argumento. Esto efectivamente "movería" el temporal al objeto que se está construyendo, eliminando así la copia innecesaria.
El constructor de movimiento se usaría para temporales y para referencias de valor l no constantes que se convierten explícitamente en referencias de valor r mediante la std::move
función (solo realiza la conversión). El siguiente código invoca el constructor de movimiento for f1
y f2
:
foo f1((foo())); // Move a temporary into f1; temporary becomes "empty"
foo f2 = std::move(f1); // Move f1 into f2; f1 is now "empty"
Reenvío perfecto . Las referencias de rvalue nos permiten reenviar correctamente argumentos para funciones con plantilla. Tomemos, por ejemplo, esta función de fábrica:
template <typename T, typename A1>
std::unique_ptr<T> factory(A1& a1)
{
return std::unique_ptr<T>(new T(a1));
}
Si llamamos a factory<foo>(5)
, se deducirá que el argumento es int&
, lo que no se vinculará a un literal 5, incluso si foo
el constructor de toma un int
. Bueno, en su lugar podríamos usar A1 const&
, pero ¿qué pasa si foo
toma el argumento del constructor mediante una referencia no constante? Para crear una función de fábrica verdaderamente genérica, tendríamos que sobrecargar la fábrica una A1&
y otra vez A1 const&
. Eso podría estar bien si la fábrica toma 1 tipo de parámetro, pero cada tipo de parámetro adicional multiplicaría la sobrecarga necesaria establecida por 2. Eso es muy rápidamente imposible de mantener.
Las referencias de rvalue solucionan este problema al permitir que la biblioteca estándar defina una std::forward
función que pueda reenviar correctamente referencias de lvalue/rvalue. Para obtener más información sobre cómo std::forward
funciona, consulte esta excelente respuesta .
Esto nos permite definir la función de fábrica de esta manera:
template <typename T, typename A1>
std::unique_ptr<T> factory(A1&& a1)
{
return std::unique_ptr<T>(new T(std::forward<A1>(a1)));
}
Ahora el valor rvalue/lvalue del argumento se conserva cuando se pasa al T
constructor. Eso significa que si se llama a la fábrica con un valor r, T
se llama al constructor de con un valor r. Si se llama a la fábrica con un valor l, T
se llama al constructor de con un valor l. La función de fábrica mejorada funciona gracias a una regla especial:
Cuando el tipo de parámetro de función tiene la forma
T&&
dondeT
es un parámetro de plantilla y el argumento de función es un valor l de tipoA
, el tipoA&
se utiliza para la deducción de argumentos de plantilla.
Por lo tanto, podemos usar factory así:
auto p1 = factory<foo>(foo()); // calls foo(foo&&)
auto p2 = factory<foo>(*p1); // calls foo(foo const&)
Propiedades importantes de referencia de rvalue :
- Para la resolución de sobrecarga, lvalues prefiere vincularse a referencias de lvalue y rvalues prefiere vincularse a referencias de rvalue . De ahí por qué los temporales prefieren invocar un constructor de movimiento/operador de asignación de movimiento en lugar de un constructor de copia/operador de asignación.
- Las referencias de rvalue se vincularán implícitamente a rvalues y a temporales que sean el resultado de una conversión implícita . ie
float f = 0f; int&& i = f;
está bien formado porque float es implícitamente convertible a int; la referencia sería a un temporal que es el resultado de la conversión. - Las referencias de rvalue con nombre son lvalues. Las referencias de rvalue sin nombre son rvalues. Esto es importante para entender por qué la
std::move
llamada es necesaria en:foo&& r = foo(); foo f = std::move(r);
Denota una referencia de valor. Las referencias de Rvalue solo se vincularán a objetos temporales, a menos que se generen explícitamente lo contrario. Se utilizan para hacer que los objetos sean mucho más eficientes en determinadas circunstancias y para proporcionar una función conocida como reenvío perfecto, que simplifica enormemente el código de plantilla.
En C++03, no se puede distinguir entre una copia de un valor l no mutable y un valor r.
std::string s;
std::string another(s); // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(const std::string&);
En C++ 0x, este no es el caso.
std::string s;
std::string another(s); // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(std::string&&);
Considere la implementación detrás de estos constructores. En el primer caso, la cadena debe realizar una copia para conservar la semántica del valor, lo que implica una nueva asignación de montón. Sin embargo, en el segundo caso, sabemos de antemano que el objeto que se pasó a nuestro constructor debe ser destruido inmediatamente y no tiene por qué permanecer intacto. En este escenario, podemos simplemente intercambiar los punteros internos y no realizar ninguna copia, lo cual es sustancialmente más eficiente. La semántica de movimiento beneficia a cualquier clase que tenga una copia costosa o prohibida de recursos a los que se hace referencia internamente. Considere el caso de std::unique_ptr
: ahora que nuestra clase puede distinguir entre temporales y no temporales, podemos hacer que la semántica de movimiento funcione correctamente para que unique_ptr
no se pueda copiar pero se pueda mover, lo que significa que std::unique_ptr
se puede almacenar legalmente en contenedores estándar, ordenar, etc, mientras que los de C++03 std::auto_ptr
no pueden.
Ahora consideramos el otro uso de las referencias de rvalue: el reenvío perfecto. Considere la cuestión de vincular una referencia a una referencia.
std::string s;
std::string& ref = s;
(std::string&)& anotherref = ref; // usually expressed via template
No recuerdo lo que dice C++03 sobre esto, pero en C++0x, el tipo resultante cuando se trata de referencias de rvalue es fundamental. Una referencia de valor a un tipo T, donde T es un tipo de referencia, se convierte en una referencia de tipo T.
(std::string&)&& ref // ref is std::string&
(const std::string&)&& ref // ref is const std::string&
(std::string&&)&& ref // ref is std::string&&
(const std::string&&)&& ref // ref is const std::string&&
Considere la función de plantilla más simple: mínima y máxima. En C++03 tienes que sobrecargar manualmente las cuatro combinaciones de constante y no constante. En C++ 0x es solo una sobrecarga. Combinado con plantillas variadas, esto permite un reenvío perfecto.
template<typename A, typename B> auto min(A&& aref, B&& bref) {
// for example, if you pass a const std::string& as first argument,
// then A becomes const std::string& and by extension, aref becomes
// const std::string&, completely maintaining it's type information.
if (std::forward<A>(aref) < std::forward<B>(bref))
return std::forward<A>(aref);
else
return std::forward<B>(bref);
}
Dejé de lado la deducción del tipo de retorno porque no recuerdo cómo se hace de manera improvisada, pero ese mínimo puede aceptar cualquier combinación de valores l, valores r y valores l constantes.
El término T&&
cuando se usa con deducción de tipo (como para reenvío perfecto) se conoce coloquialmente como referencia de reenvío . El término "referencia universal" fue acuñado por Scott Meyers en este artículo , pero luego fue cambiado.
Esto se debe a que puede ser el valor r o el valor l.
Ejemplos son:
// template
template<class T> foo(T&& t) { ... }
// auto
auto&& t = ...;
// typedef
typedef ... T;
T&& t = ...;
// decltype
decltype(...)&& t = ...;
Se puede encontrar más discusión en la respuesta para: Sintaxis para referencias universales