Mi primera respuesta fue una introducción extremadamente simplificada para mover la semántica, y se omitieron muchos detalles a propósito para que fuera simple. Sin embargo, hay mucho más para mover la semántica, y pensé que era hora de una segunda respuesta para llenar los vacíos. La primera respuesta ya es bastante antigua, y no me pareció correcto simplemente reemplazarla con un texto completamente diferente. Creo que todavía sirve como una primera introducción. Pero si quieres profundizar más, sigue leyendo :)
Stephan T. Lavavej se tomó el tiempo para proporcionar comentarios valiosos. Muchas gracias, Stephan!
Introducción
La semántica de movimiento permite que un objeto, bajo ciertas condiciones, tome posesión de los recursos externos de otro objeto. Esto es importante de dos maneras:
Convirtiendo copias caras en movimientos baratos. Vea mi primera respuesta para un ejemplo. Tenga en cuenta que si un objeto no administra al menos un recurso externo (ya sea directa o indirectamente a través de sus objetos miembros), la semántica de movimiento no ofrecerá ninguna ventaja sobre la semántica de copia. En ese caso, copiar un objeto y mover un objeto significa exactamente lo mismo:
class cannot_benefit_from_move_semantics
{
int a; // moving an int means copying an int
float b; // moving a float means copying a float
double c; // moving a double means copying a double
char d[64]; // moving a char array means copying a char array
// ...
};
Implementación de tipos seguros de "solo movimiento"; es decir, los tipos para los que copiar no tiene sentido, pero el movimiento sí. Los ejemplos incluyen bloqueos, identificadores de archivos e indicadores inteligentes con una semántica de propiedad única. Nota: Esta respuesta trata sobre std::auto_ptr
una plantilla de biblioteca estándar C ++ 98 en desuso, que fue reemplazada porstd::unique_ptr
en C ++ 11. Los programadores intermedios de C ++ probablemente estén al menos algo familiarizados std::auto_ptr
, y debido a la "semántica de movimiento" que muestra, parece un buen punto de partida para discutir la semántica de movimiento en C ++ 11. YMMV.
¿Qué es un movimiento?
La biblioteca estándar C ++ 98 ofrece un puntero inteligente con una semántica de propiedad única llamada std::auto_ptr<T>
. En caso de que no esté familiarizado auto_ptr
, su propósito es garantizar que siempre se libere un objeto asignado dinámicamente, incluso ante excepciones:
{
std::auto_ptr<Shape> a(new Triangle);
// ...
// arbitrary code, could throw exceptions
// ...
} // <--- when a goes out of scope, the triangle is deleted automatically
Lo inusual auto_ptr
es su comportamiento de "copia":
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
Nota cómo la inicialización de b
con a
qué no copiar el triángulo, sino que transfiere la propiedad del triángulo de a
a b
. También decimos " a
se mueve a b
" o "el triángulo se mueve de a
a b
". Esto puede sonar confuso porque el triángulo siempre permanece en el mismo lugar en la memoria.
Mover un objeto significa transferir la propiedad de un recurso que administra a otro objeto.
El constructor de copia de auto_ptr
probablemente se parece a esto (algo simplificado):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
Movimientos peligrosos e inofensivos.
Lo peligroso de esto auto_ptr
es que lo que parece sintácticamente una copia es en realidad un movimiento. Intentar llamar a una función miembro en un auto_ptr
traspaso invocará un comportamiento indefinido, por lo que debe tener mucho cuidado de no usar un auto_ptr
después de que se haya movido desde:
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
Pero auto_ptr
no siempre es peligroso. Las funciones de fábrica son un caso de uso perfecto para auto_ptr
:
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
Observe cómo ambos ejemplos siguen el mismo patrón sintáctico:
auto_ptr<Shape> variable(expression);
double area = expression->area();
Y, sin embargo, uno de ellos invoca un comportamiento indefinido, mientras que el otro no. Entonces, ¿cuál es la diferencia entre las expresiones a
y make_triangle()
? ¿No son los dos del mismo tipo? De hecho lo son, pero tienen diferentes categorías de valor .
Categorías de valor
Obviamente, debe haber alguna diferencia profunda entre la expresión a
que denota una auto_ptr
variable y la expresión make_triangle()
que denota la llamada de una función que devuelve un auto_ptr
by, creando así un nuevo auto_ptr
objeto temporal cada vez que se llama. a
es un ejemplo de un lvalue , mientras que make_triangle()
es un ejemplo de un rvalue .
Pasar de valores como a
es peligroso, ya que más tarde podríamos intentar llamar a una función miembro a
invocando un comportamiento indefinido. Por otro lado, pasar de valores como make_triangle()
es perfectamente seguro, porque después de que el constructor de copia haya hecho su trabajo, no podemos usar el temporal nuevamente. No hay expresión que denote dicho temporal; Si simplemente escribimos de make_triangle()
nuevo, obtenemos un temporal diferente . De hecho, el traslado temporal ya no está en la siguiente línea:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
Tenga en cuenta que las letras l
yr
tienen un origen histórico en el lado izquierdo y en el lado derecho de una tarea. Esto ya no es cierto en C ++, porque hay valores que no pueden aparecer en el lado izquierdo de una asignación (como matrices o tipos definidos por el usuario sin un operador de asignación), y hay valores que pueden (todos los valores de los tipos de clase con un operador de asignación).
Un valor de tipo de clase es una expresión cuya evaluación crea un objeto temporal. En circunstancias normales, ninguna otra expresión dentro del mismo alcance denota el mismo objeto temporal.
Referencias de valor
Ahora entendemos que moverse de los valores es potencialmente peligroso, pero moverse de los valores es inofensivo. Si C ++ tuviera soporte de lenguaje para distinguir los argumentos de lvalue de los argumentos de rvalue, podríamos prohibir completamente el movimiento de los valores, o al menos hacer que el movimiento de los valores sea explícito en el sitio de la llamada, de modo que ya no nos por accidente.
La respuesta de C ++ 11 a este problema son las referencias de valor . Una referencia de valor r es un nuevo tipo de referencia que solo se une a valores r, y la sintaxis es X&&
. La buena referencia antigua X&
ahora se conoce como referencia lvalue . (Tenga en cuenta que noX&&
es una referencia a una referencia; no existe tal cosa en C ++).
Si nos adentramos const
en la mezcla, ya tenemos cuatro tipos diferentes de referencias. ¿A qué tipos de expresiones de tipo se X
pueden unir?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
En la práctica, te puedes olvidar const X&&
. Estar restringido para leer valores no es muy útil.
Una referencia de valor X&&
es un nuevo tipo de referencia que solo se une a los valores.
Conversiones implícitas
Las referencias de Rvalue pasaron por varias versiones. Desde la versión 2.1, una referencia de valor X&&
también se une a todas las categorías de valores de un tipo diferente Y
, siempre que haya una conversión implícita de Y
a X
. En ese caso, X
se crea un tipo temporal , y la referencia rvalue está vinculada a ese temporal:
void some_function(std::string&& r);
some_function("hello world");
En el ejemplo anterior, "hello world"
es un valor de tipo l const char[12]
. Dado que no hay una conversión implícita de const char[12]
a través const char*
de std::string
, un temporal de tipo std::string
se crea, yr
está obligado a que temporal. Este es uno de los casos en los que la distinción entre valores (expresiones) y temporales (objetos) es un poco borrosa.
Mover constructores
Un ejemplo útil de una función con un X&&
parámetro es el constructor de movimiento X::X(X&& source)
. Su propósito es transferir la propiedad del recurso administrado desde la fuente al objeto actual.
En C ++ 11, std::auto_ptr<T>
ha sido reemplazado por el std::unique_ptr<T>
que aprovecha las referencias rvalue. Desarrollaré y discutiré una versión simplificada de unique_ptr
. Primero, encapsulamos un puntero sin formato y sobrecargamos los operadores ->
y *
, por lo tanto, nuestra clase se siente como un puntero:
template<typename T>
class unique_ptr
{
T* ptr;
public:
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
El constructor toma posesión del objeto y el destructor lo elimina:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
Ahora viene la parte interesante, el constructor de movimiento:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
Este constructor de movimientos hace exactamente lo que hizo el auto_ptr
constructor de copias, pero solo se puede suministrar con valores:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
La segunda línea no se compila, porque a
es un valor l, pero el parámetro unique_ptr&& source
solo puede vincularse a valores. Esto es exactamente lo que queríamos; movimientos peligrosos nunca deben ser implícitos. La tercera línea compila muy bien, porque make_triangle()
es un valor r. El movimiento constructor transferirá la propiedad de la que temporal c
. De nuevo, esto es exactamente lo que queríamos.
El constructor de movimiento transfiere la propiedad de un recurso administrado al objeto actual.
Mover operadores de asignación
La última pieza que falta es el operador de asignación de movimiento. Su trabajo es liberar el antiguo recurso y adquirir el nuevo recurso de su argumento:
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
}
};
Observe cómo esta implementación del operador de asignación de movimiento duplica la lógica tanto del destructor como del constructor de movimiento. ¿Conoces el idioma de copiar y cambiar? También se puede aplicar para mover la semántica como el modismo de mover y cambiar:
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
Ahora que source
es una variable de tipo unique_ptr
, será inicializada por el constructor de movimiento; es decir, el argumento se moverá al parámetro. Todavía se requiere que el argumento sea un valor r, porque el constructor de movimiento tiene un parámetro de referencia rvalue. Cuando el flujo de control alcanza la llave de cierre operator=
, source
se sale del alcance y libera el recurso antiguo automáticamente.
El operador de asignación de movimiento transfiere la propiedad de un recurso administrado al objeto actual, liberando el recurso anterior. El modismo move-and-swap simplifica la implementación.
Mudarse de lvalues
A veces, queremos pasar de los valores. Es decir, a veces queremos que el compilador trate un valor l como si fuera un valor r, por lo que puede invocar al constructor de movimiento, aunque pueda ser potencialmente inseguro. Para este propósito, C ++ 11 ofrece una plantilla de función de biblioteca estándar llamada std::move
dentro del encabezado <utility>
. Este nombre es un poco desafortunado, porque std::move
simplemente arroja un valor l a un valor r; sí no se mueve nada por sí misma. Simplemente permite moverse. Tal vez debería haber sido nombrado std::cast_to_rvalue
o std::enable_move
, pero ahora estamos atrapados con el nombre.
Así es como se mueve explícitamente desde un valor l:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
Tenga en cuenta que después de la tercera línea, a
ya no posee un triángulo. Está bien, porque al escribir explícitamentestd::move(a)
, dejamos en claro nuestras intenciones: "Estimado constructor, haz lo que quieras a
para inicializar c
; ya no me importa a
. Siéntete libre de seguir tu camino a
".
std::move(some_lvalue)
lanza un valor l a un valor r, permitiendo así un movimiento posterior.
Xvalores
Tenga en cuenta que aunque std::move(a)
es un valor r, su evaluación no crea un objeto temporal. Este enigma obligó al comité a introducir una tercera categoría de valor. Algo que se puede unir a una referencia de valor de lado derecho, a pesar de que no es un valor de lado derecho en el sentido tradicional, se denomina xValue (valor que expira). Los valores tradicionales fueron renombrados como valores ( valores puros).
Tanto los valores como los valores x son valores. Los valores X y los valores son ambos valores (valores generalizados). Las relaciones son más fáciles de comprender con un diagrama:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Tenga en cuenta que solo los valores x son realmente nuevos; el resto solo se debe a renombrar y agrupar.
Los valores de C ++ 98 se conocen como valores en C ++ 11. Reemplace mentalmente todas las ocurrencias de "rvalue" en los párrafos anteriores con "prvalue".
Salir de funciones
Hasta ahora, hemos visto movimientos en variables locales y en parámetros de función. Pero moverse también es posible en la dirección opuesta. Si una función regresa por valor, algún objeto en el sitio de la llamada (probablemente una variable local o temporal, pero podría ser cualquier tipo de objeto) se inicializa con la expresión después de la return
declaración como argumento para el constructor de movimiento:
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
Quizás sorprendentemente, los objetos automáticos (variables locales que no se declaran como static
) también se pueden mover implícitamente fuera de las funciones:
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // note the missing std::move
}
¿Cómo es que el constructor de movimientos acepta el valor l result
como argumento? El alcance de result
está a punto de finalizar, y se destruirá durante el desbobinado de la pila. Nadie podría quejarse después de que eso result
hubiera cambiado de alguna manera; cuando el flujo de control vuelve a la persona que llama, ¡ result
ya no existe! Por esa razón, C ++ 11 tiene una regla especial que permite devolver objetos automáticos de funciones sin tener que escribir std::move
. De hecho, nunca debe usar std::move
para mover objetos automáticos fuera de las funciones, ya que esto inhibe la "optimización del valor de retorno con nombre" (NRVO).
Nunca use std::move
para mover objetos automáticos fuera de las funciones.
Tenga en cuenta que en ambas funciones de fábrica, el tipo de retorno es un valor, no una referencia de valor. Las referencias de Rvalue siguen siendo referencias y, como siempre, nunca debe devolver una referencia a un objeto automático; la persona que llama terminaría con una referencia pendiente si engañaste al compilador para que acepte tu código, así:
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // WRONG!
}
Nunca devuelva objetos automáticos por referencia de valor. El movimiento se realiza exclusivamente por el constructor del movimiento, no por std::move
, y no simplemente vinculando un valor r a una referencia de valor r.
Pasando a miembros
Tarde o temprano, vas a escribir código como este:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
Básicamente, el compilador se quejará de que parameter
es un valor. Si observa su tipo, verá una referencia de valor r, pero una referencia de valor simplemente significa "una referencia vinculada a un valor r"; ¡ no significa que la referencia en sí misma sea un valor! De hecho, parameter
es solo una variable ordinaria con un nombre. Puede usar parameter
tantas veces como desee dentro del cuerpo del constructor, y siempre denota el mismo objeto. Irse de manera implícita sería peligroso, de ahí que el lenguaje lo prohíba.
Una referencia de rvalue con nombre es un lvalue, como cualquier otra variable.
La solución es habilitar manualmente el movimiento:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(std::move(parameter)) // note the std::move
{}
};
Se podría argumentar que parameter
ya no se usa después de la inicialización de member
. ¿Por qué no hay una regla especial para insertar silenciosamentestd::move
como con los valores de retorno? Probablemente porque sería una carga excesiva para los implementadores del compilador. Por ejemplo, ¿qué pasa si el cuerpo del constructor estaba en otra unidad de traducción? Por el contrario, la regla del valor de retorno simplemente tiene que verificar las tablas de símbolos para determinar si el identificador después de que la return
palabra clave denota un objeto automático.
También puede pasar el parameter
por valor. Para tipos de solo movimiento comounique_ptr
, parece que todavía no hay un idioma establecido. Personalmente, prefiero pasar por valor, ya que causa menos desorden en la interfaz.
Funciones especiales para miembros
C ++ 98 declara implícitamente tres funciones miembro especiales a pedido, es decir, cuando se necesitan en algún lugar: el constructor de copia, el operador de asignación de copia y el destructor.
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
Las referencias de Rvalue pasaron por varias versiones. Desde la versión 3.0, C ++ 11 declara dos funciones miembro especiales adicionales a pedido: el constructor de movimientos y el operador de asignación de movimientos. Tenga en cuenta que ni VC10 ni VC11 se ajustan a la versión 3.0 todavía, por lo que deberá implementarlos usted mismo.
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
Estas dos nuevas funciones miembro especiales solo se declaran implícitamente si ninguna de las funciones miembro especiales se declara manualmente. Además, si declara su propio constructor de movimientos u operador de asignación de movimientos, ni el constructor de copias ni el operador de asignación de copias se declararán implícitamente.
¿Qué significan estas reglas en la práctica?
Si escribe una clase sin recursos no administrados, no es necesario declarar ninguna de las cinco funciones especiales para miembros, y obtendrá la semántica de copia correcta y moverá la semántica de forma gratuita. De lo contrario, deberá implementar las funciones especiales para miembros usted mismo. Por supuesto, si su clase no se beneficia de la semántica de movimiento, no hay necesidad de implementar las operaciones especiales de movimiento.
Tenga en cuenta que el operador de asignación de copia y el operador de asignación de movimiento pueden fusionarse en un único operador de asignación unificado, tomando su argumento por valor:
X& X::operator=(X source) // unified assignment operator
{
swap(source); // see my first answer for an explanation
return *this;
}
De esta forma, el número de funciones miembro especiales para implementar cae de cinco a cuatro. Aquí hay una compensación entre seguridad de excepción y eficiencia, pero no soy un experto en este tema.
Reenvío de referencias ( anteriormente conocidas como referencias universales )
Considere la siguiente plantilla de función:
template<typename T>
void foo(T&&);
Puede esperar T&&
que solo se una a los valores, porque a primera vista, parece una referencia de valor. Sin embargo, resulta que T&&
también se une a los valores:
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
Si el argumento es un valor de tipo X
, T
se deduce que es X
, por T&&
lo tanto, significa X&&
. Esto es lo que cualquiera esperaría. Pero si el argumento es un valor de tipo X
, debido a una regla especial, T
se deduce que es X&
, por T&&
lo tanto , significaría algo así X& &&
. Pero puesto que C ++ todavía no tiene noción de referencias a referencias, el tipo X& &&
está colapsado en X&
. Esto puede sonar confuso e inútil al principio, pero el colapso de referencia es esencial para un reenvío perfecto (que no se discutirá aquí).
T&& no es una referencia de valor, sino una referencia de reenvío. También se une a los valores, en cuyo caso T
y T&&
son referencias de ambos valores.
Si desea restringir una plantilla de función a valores, puede combinar SFINAE con rasgos de tipo:
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
Implementación de movimiento
Ahora que comprende el colapso de referencias, así es como std::move
se implementa:
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Como puede ver, move
acepta cualquier tipo de parámetro gracias a la referencia de reenvío T&&
y devuelve una referencia de valor. La std::remove_reference<T>::type
llamada a la metafunción es necesaria porque de lo contrario, para valores de tipo X
, el tipo de retorno sería X& &&
, que colapsaría X&
. Como t
siempre es un valor de l (recuerde que una referencia de valor de r con nombre es un valor de l), pero queremos vincularnos t
a una referencia de valor de r, tenemos que convertir explícitamente t
al tipo de retorno correcto. La llamada de una función que devuelve una referencia rvalue es en sí misma un xvalue. Ahora ya sabes de dónde vienen los valores x;)
La llamada de una función que devuelve una referencia rvalue, como std::move
, es un xvalue.
Tenga en cuenta que la devolución por referencia rvalue está bien en este ejemplo, porque t
no denota un objeto automático, sino un objeto que fue pasado por la persona que llama.