Declarador de referencia a un valor R: &&

Mantiene una referencia a una expresión de valor R.

Sintaxis

rvalue-reference-type-id:
type-specifier-seq&&attribute-specifier-seqoptptr-abstract-declaratoropt

Comentarios

Las referencias de valor R permiten distinguir un valor L de un valor R Las referencias de valor L y valor R son sintáctica y semánticamente similares, pero siguen reglas algo distintas. Para obtener más información sobre valores L y valores R, vea Lvalues y Rvalues. Para obtener más información sobre las referencias lvalue, vea Lvalue Reference Declarator: &.

Las secciones siguientes describen cómo las referencias de valor R admiten la implementación de semántica de transferencia de recursos y reenvío directo.

Semántica de transferencia de recursos

Las referencias de valor R admiten la implementación de la semántica de transferencia de recursos, que puede aumentar significativamente el rendimiento de las aplicaciones. La semántica de transferencia de recursos permite escribir código que transfiere recursos (tales como memoria asignada dinámicamente) de un objeto a otro. La semántica de transferencia de recursos funciona porque permite transferir recursos desde objetos temporales a los que no se puede hacer referencia en otra parte del programa.

Para implementar la semántica de transferencia de recursos, normalmente se proporciona un constructor de movimiento y opcionalmente, un operador de asignación de movimiento (operator=) a la clase. Las operaciones de copia y asignación cuyos orígenes son valores R aprovechan entonces automáticamente la semántica de transferencia de recursos. A diferencia del constructor de copia predeterminado, el compilador no proporciona un constructor de movimiento predeterminado. Para obtener más información sobre cómo escribir y usar un constructor de movimiento, vea Constructores de movimiento y operadores de asignación de movimiento.

También puede sobrecargar funciones y operadores normales para aprovechar la semántica de transferencia de recursos. Visual Studio 2010 presenta la semántica de movimiento a la biblioteca estándar de C++. Por ejemplo, la clase string implementa operaciones que usan semántica de transferencia de recursos. Considere el ejemplo siguiente que concatena varias cadenas e imprime el resultado:

// string_concatenation.cpp
// compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;

int main()
{
   string s = string("h") + "e" + "ll" + "o";
   cout << s << endl;
}

Antes de Visual Studio 2010, cada llamada a operator+ asigna y devuelve un nuevo objeto string temporal (un valor R). operator+ no puede anexar una cadena a la otra porque no sabe si las cadenas de origen no son valores L o valores R. Si las cadenas de origen son valores lvalues, se pueden hacer referencia a otras partes del programa y, por tanto, no deben modificarse. Puede modificar operator+ para tomar valores r mediante referencias de rvalue, a las que no se puede hacer referencia en ningún otro lugar del programa. Con este cambio, operator+ ahora puede anexar una cadena a otra. El cambio reduce significativamente el número de asignaciones de memoria dinámica que la clase string debe realizar. Para obtener más información sobre la string clase , vea basic_string Clase.

La semántica de transferencia de recursos también ayuda cuando el compilador no puede utilizar la optimización del valor devuelto (RVO) o la optimización del valor devuelto con nombre (NRVO). En estos casos, el compilador llama al constructor de movimiento si el tipo lo define.

Para entender mejor la semántica de transferencia de recursos, considere el ejemplo de la inserción de un elemento en un objeto vector. Si se supera la capacidad del vector objeto, el vector objeto debe reasignar suficiente memoria para sus elementos y, a continuación, copiar cada elemento en otra ubicación de memoria para dejar espacio para el elemento insertado. Cuando una operación de inserción copia un elemento, primero crea un nuevo elemento. A continuación, llama al constructor de copia para copiar los datos del elemento anterior al nuevo elemento. Por último, destruye el elemento anterior. La semántica de transferencia de recursos permite mover objetos directamente sin tener que realizar una asignación de gran consumo de memoria ni operaciones de copia.

Para aprovechar la semántica de transferencia de recursos en el ejemplo vector, puede escribir un constructor de movimiento para mover los datos de un objeto a otro.

Para obtener más información sobre la introducción de la semántica de movimiento a la biblioteca estándar de C++ en Visual Studio 2010, vea Biblioteca estándar de C++.

Reenvío perfecto

El reenvío directo reduce la necesidad de funciones sobrecargadas y ayuda a evitar el problema de reenvío. El problema de reenvío puede ocurrir cuando se escribe una función genérica que acepta referencias como parámetros. Si pasa (o reenvía) estos parámetros a otra función, por ejemplo, si toma un parámetro de tipo const T&, la función llamada no puede modificar el valor de ese parámetro. Si la función genérica toma un parámetro de tipo T&, no se puede llamar a la función mediante un valor R (tal como un objeto temporal o un literal entero).

Normalmente, para solucionar este problema, debe proporcionar versiones sobrecargadas de la función genérica que acepten tanto T& como const T& para cada uno de sus parámetros. Como resultado, el número de funciones sobrecargadas aumenta exponencialmente con el número de parámetros. Las referencias de rvalue permiten escribir una versión de una función que acepta argumentos arbitrarios. Después, esa función puede reenviarlas a otra función como si se hubiera llamado directamente a la otra función.

Considere el ejemplo siguiente en el que se declaran cuatro tipos, W, X, Y y Z. El constructor de cada tipo toma una combinación distinta de referencias de lvalue const y no const como parámetros.

struct W
{
   W(int&, int&) {}
};

struct X
{
   X(const int&, int&) {}
};

struct Y
{
   Y(int&, const int&) {}
};

struct Z
{
   Z(const int&, const int&) {}
};

Supongamos que desea escribir una función genérica que genere objetos. En el siguiente ejemplo, se muestra una forma de escribir esta función:

template <typename T, typename A1, typename A2>
T* factory(A1& a1, A2& a2)
{
   return new T(a1, a2);
}

En el ejemplo siguiente se muestra una llamada válida a la función factory.

int a = 4, b = 5;
W* pw = factory<W>(a, b);

Sin embargo, el ejemplo siguiente no contiene una llamada válida a la función factory. Se debe a factory que toma referencias lvalue que se pueden modificar como parámetros, pero se llama mediante rvalues:

Z* pz = factory<Z>(2, 2);

Normalmente, para solucionar este problema se debe crear una versión sobrecargada de la función factory para cada combinación de los parámetros A& y const A&. Las referencias de valor R permiten escribir una versión de la función factory, como se muestra en el ejemplo siguiente:

template <typename T, typename A1, typename A2>
T* factory(A1&& a1, A2&& a2)
{
   return new T(std::forward<A1>(a1), std::forward<A2>(a2));
}

En este ejemplo se usan referencias de valor R como parámetros para la función factory. El propósito de la std::forward función es reenviar los parámetros de la función factory al constructor de la clase de plantilla.

En el ejemplo siguiente se muestra la función main que usa la función factory revisada para crear instancias de las clases W, XY y Z. La función factory revisada reenvía sus parámetros (ya sean valores L o valores R) al constructor de clase adecuado.

int main()
{
   int a = 4, b = 5;
   W* pw = factory<W>(a, b);
   X* px = factory<X>(2, b);
   Y* py = factory<Y>(a, 2);
   Z* pz = factory<Z>(2, 2);

   delete pw;
   delete px;
   delete py;
   delete pz;
}

Propiedades de referencias rvalue

Puede sobrecargar una función para tomar una referencia lvalue y una referencia rvalue.

Mediante la sobrecarga de una función para que acepte una referencia de lvalue o una referencia de rvalue const, puede escribir código que distinga entre objetos no modificables (lvalue) y valores temporales modificables (rvalue). Puede pasar un objeto a una función que acepte una referencia de valor a menos que el objeto esté marcado como const. El ejemplo siguiente muestra la función f, que se sobrecarga para aceptar una referencia de valor L y una referencia de valor R. La función main llama a f con ambos valores L y un valor R.

// reference-overload.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void f(const MemoryBlock&)
{
   cout << "In f(const MemoryBlock&). This version can't modify the parameter." << endl;
}

void f(MemoryBlock&&)
{
   cout << "In f(MemoryBlock&&). This version can modify the parameter." << endl;
}

int main()
{
   MemoryBlock block;
   f(block);
   f(MemoryBlock());
}

Este ejemplo produce el siguiente resultado:

In f(const MemoryBlock&). This version can't modify the parameter.
In f(MemoryBlock&&). This version can modify the parameter.

En este ejemplo, la primera llamada a f pasa una variable local (un valor L) como argumento. La segunda llamada a f pasa un objeto temporal como argumento. Como no se puede hacer referencia al objeto temporal en otra parte del programa, la llamada se enlaza a la versión sobrecargada de f que acepta una referencia de rvalue, que es libre de modificar el objeto.

El compilador trata una referencia de rvalue con nombre como un valor lvalue y una referencia rvalue sin nombre como rvalue.

Las funciones que toman una referencia rvalue como parámetro tratan el parámetro como un valor l en el cuerpo de la función. El compilador trata una referencia con nombre rvalue como un valor lvalue. Se debe a que varias partes de un programa pueden hacer referencia a un objeto con nombre. Es peligroso permitir que varias partes de un programa modifiquen o quiten recursos de ese objeto. Por ejemplo, si varias partes de un programa intentan transferir recursos del mismo objeto, solo la primera parte transferirá el recurso correctamente.

El ejemplo siguiente muestra la función g, que se sobrecarga para aceptar una referencia de valor L y una referencia de valor R. La función f acepta una referencia de valor R como parámetro (una referencia de valor R con nombre) y devuelve una referencia de valor R (una referencia de valor R sin nombre). En la llamada a g desde f, la resolución de sobrecarga selecciona la versión de g que acepta una referencia de valor L porque el cuerpo de f considera el parámetro como un valor L. En la llamada a g desde main, la resolución de sobrecarga selecciona la versión de g que acepta una referencia de valor R porque f devuelve una referencia de valor R.

// named-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void g(const MemoryBlock&)
{
   cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
   cout << "In g(MemoryBlock&&)." << endl;
}

MemoryBlock&& f(MemoryBlock&& block)
{
   g(block);
   return move(block);
}

int main()
{
   g(f(MemoryBlock()));
}

Este ejemplo produce el siguiente resultado:

In g(const MemoryBlock&).
In g(MemoryBlock&&).

En este ejemplo, la función main pasa un rvalue a f. El cuerpo de f trata el parámetro con nombre como valor L. La llamada de f a g enlaza el parámetro a una referencia de valor L (la primera versión sobrecargada de g).

  • Puede convertir un valor lvalue en una referencia rvalue.

La función std::move de Biblioteca estándar de C++ permite convertir un objeto en una referencia de valor R a ese objeto. También puede usar la palabra clave static_cast para convertir un lvalue en una referencia de rvalue, como se muestra en el ejemplo siguiente:

// cast-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void g(const MemoryBlock&)
{
   cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
   cout << "In g(MemoryBlock&&)." << endl;
}

int main()
{
   MemoryBlock block;
   g(block);
   g(static_cast<MemoryBlock&&>(block));
}

Este ejemplo produce el siguiente resultado:

In g(const MemoryBlock&).
In g(MemoryBlock&&).

Las plantillas de función deducen sus tipos de argumentos de plantilla y, a continuación, usan reglas de contracción de referencia.

Una plantilla de función que pase (o reenvíe) sus parámetros a otra función es un patrón común. Es importante saber cómo funciona la deducción de tipos de plantilla para plantillas de función que acepten referencias de rvalue.

Si el argumento de función es un valor R, el compilador deduce que el argumento será una referencia de valor R. Por ejemplo, supongamos que pasa una referencia rvalue a un objeto de tipo X a una plantilla de función que toma el tipo T&& como parámetro. La deducción del argumento de plantilla deduce T que es X, por lo que el parámetro tiene el tipo X&&. Si el argumento de función es un lvalue o un lvalue const, el compilador deduce que su tipo será una referencia de lvalue const o una referencia de lvalue de ese tipo.

En el ejemplo siguiente se declara una plantilla de estructura y, a continuación, se especializa para distintos tipos de referencia. La función print_type_and_value acepta una referencia de valor R como parámetro y lo reenvía a la versión especializada adecuada del método S::print. La función main muestra las diversas maneras de llamar al método S::print.

// template-type-deduction.cpp
// Compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;

template<typename T> struct S;

// The following structures specialize S by
// lvalue reference (T&), const lvalue reference (const T&),
// rvalue reference (T&&), and const rvalue reference (const T&&).
// Each structure provides a print method that prints the type of
// the structure and its parameter.

template<typename T> struct S<T&> {
   static void print(T& t)
   {
      cout << "print<T&>: " << t << endl;
   }
};

template<typename T> struct S<const T&> {
   static void print(const T& t)
   {
      cout << "print<const T&>: " << t << endl;
   }
};

template<typename T> struct S<T&&> {
   static void print(T&& t)
   {
      cout << "print<T&&>: " << t << endl;
   }
};

template<typename T> struct S<const T&&> {
   static void print(const T&& t)
   {
      cout << "print<const T&&>: " << t << endl;
   }
};

// This function forwards its parameter to a specialized
// version of the S type.
template <typename T> void print_type_and_value(T&& t)
{
   S<T&&>::print(std::forward<T>(t));
}

// This function returns the constant string "fourth".
const string fourth() { return string("fourth"); }

int main()
{
   // The following call resolves to:
   // print_type_and_value<string&>(string& && t)
   // Which collapses to:
   // print_type_and_value<string&>(string& t)
   string s1("first");
   print_type_and_value(s1);

   // The following call resolves to:
   // print_type_and_value<const string&>(const string& && t)
   // Which collapses to:
   // print_type_and_value<const string&>(const string& t)
   const string s2("second");
   print_type_and_value(s2);

   // The following call resolves to:
   // print_type_and_value<string&&>(string&& t)
   print_type_and_value(string("third"));

   // The following call resolves to:
   // print_type_and_value<const string&&>(const string&& t)
   print_type_and_value(fourth());
}

Este ejemplo produce el siguiente resultado:

print<T&>: first
print<const T&>: second
print<T&&>: third
print<const T&&>: fourth

Para resolver cada llamada a la función print_type_and_value, el compilador realiza primero la deducción de argumento de plantilla. El compilador aplica entonces las reglas de colapso de referencias cuando sustituye los tipos de parámetros por los argumentos deducidos de la plantilla. Por ejemplo, al pasar la variable local s1 a la función print_type_and_value se provoca que el compilador genere la siguiente signatura de función:

print_type_and_value<string&>(string& && t)

El compilador usa reglas de contracción de referencias para reducir la signatura:

print_type_and_value<string&>(string& t)

Esta versión de la función print_type_and_value reenvía a continuación su parámetro a la versión especializada correcta del método S::print.

En la tabla siguiente se resumen las reglas de contracción de referencias para la deducción de tipos de argumento de plantilla:

Tipo expandido Tipo contraído
T& & T&
T& && T&
T&& & T&
T&& && T&&

La deducción de argumento de plantilla es un elemento importante de la implementación del reenvío directo. En la sección Reenvío perfecto se describe el reenvío perfecto con más detalle.

Resumen

Las referencias de valor R distinguen los valores L de los valores R. Para mejorar el rendimiento de las aplicaciones, se puede eliminar la necesidad de asignaciones de memoria y operaciones de copia innecesarias. También permiten escribir una función que acepte argumentos arbitrarios. Esa función puede reenviarlas a otra función como si se hubiera llamado directamente a la otra función.

Consulte también

Expresiones con operadores unarios
Declarador de referencia a un lvalue: &
Lvalues y rvalues
Constructores de movimiento y operadores de asignación de movimiento (C++)
Biblioteca estándar de C++