Es correcto que std::move(x)
sea solo un yeso para rvalue, más específicamente para un xvalue , en lugar de un prvalue . Y también es cierto que tener un elenco llamado a move
veces confunde a las personas. Sin embargo, la intención de este nombre no es confundir, sino hacer que su código sea más legible.
La historia move
se remonta a la propuesta de mudanza original en 2002 . Este artículo presenta primero la referencia de valor, y luego muestra cómo escribir un método más eficiente std::swap
:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
Hay que recordar que en este punto de la historia, lo único que " &&
" podría significar era lógico y . Nadie estaba familiarizado con las referencias de valor, ni con las implicaciones de transmitir un valor a un valor (sin hacer una copia como static_cast<T>(t)
lo haría). Entonces, los lectores de este código naturalmente pensarían:
Sé cómo swap
se supone que funciona (copie en temporal y luego intercambie los valores), pero ¿cuál es el propósito de esos modelos feos?
Tenga en cuenta también que en swap
realidad es solo un sustituto para todo tipo de algoritmos de modificación de permutación. Esta discusión es mucho , mucho más grande que swap
.
Luego, la propuesta introduce el azúcar de sintaxis que reemplaza el static_cast<T&&>
con algo más legible que transmite no el qué exacto , sino el por qué :
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Es decir, move
es sintaxis de azúcar para static_cast<T&&>
, y ahora el código es bastante sugerente de por qué esos lanzamientos están ahí: ¡para permitir la semántica de movimiento!
Hay que entender que, en el contexto de la historia, pocas personas en este punto realmente entendieron la conexión íntima entre los valores y la semántica del movimiento (aunque el documento intenta explicar eso también):
La semántica de movimiento entrará automáticamente en juego cuando se le den argumentos de valor. Esto es perfectamente seguro porque el resto del programa no puede notar mover recursos de un valor r ( nadie más tiene una referencia al valor r para detectar una diferencia ).
Si en ese momento swap
se presentó así:
template <class T>
void
swap(T& a, T& b)
{
T tmp(cast_to_rvalue(a));
a = cast_to_rvalue(b);
b = cast_to_rvalue(tmp);
}
Entonces la gente habría mirado eso y dicho:
Pero, ¿por qué estás evaluando?
El punto principal:
Tal como estaba, usando move
, nadie preguntó:
¿Pero por qué te mueves?
A medida que pasaron los años y la propuesta fue refinada, las nociones de lvalue y rvalue se refinaron en las categorías de valor que tenemos hoy:
(imagen robada descaradamente de dirkgently )
Y así, hoy, si quisiéramos swap
decir con precisión lo que está haciendo, en lugar de por qué , debería verse más como:
template <class T>
void
swap(T& a, T& b)
{
T tmp(set_value_category_to_xvalue(a));
a = set_value_category_to_xvalue(b);
b = set_value_category_to_xvalue(tmp);
}
Y la pregunta que todos deberían hacerse es si el código anterior es más o menos legible que:
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
O incluso el original:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
En cualquier caso, el programador de C ++ de Journeyman debe saber que, bajo el capó move
, no está pasando nada más que un elenco. Y el programador principiante de C ++, al menos con move
, será informado de que la intención es moverse de la rhs, en lugar de copiar de la rhs, incluso si no entienden exactamente cómo se logra eso.
Además, si un programador desea esta funcionalidad con otro nombre, std::move
no posee el monopolio de esta funcionalidad y no hay magia de lenguaje no portátil involucrada en su implementación. Por ejemplo, si uno quisiera codificar set_value_category_to_xvalue
, y usarlo en su lugar, es trivial hacerlo:
template <class T>
inline
constexpr
typename std::remove_reference<T>::type&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
En C ++ 14 se vuelve aún más conciso:
template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<std::remove_reference_t<T>&&>(t);
}
Por lo tanto, si está tan inclinado, decore el static_cast<T&&>
que mejor le parezca, y tal vez terminará desarrollando una nueva mejor práctica (C ++ está en constante evolución).
Entonces, ¿qué hace move
en términos de código objeto generado?
Considera esto test
:
void
test(int& i, int& j)
{
i = j;
}
Compilado con clang++ -std=c++14 test.cpp -O3 -S
, esto produce este código objeto:
__Z4testRiS_: ## @_Z4testRiS_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
movl (%rsi), %eax
movl %eax, (%rdi)
popq %rbp
retq
.cfi_endproc
Ahora si la prueba se cambia a:
void
test(int& i, int& j)
{
i = std::move(j);
}
No hay absolutamente ningún cambio en absoluto en el código objeto. Uno puede generalizar este resultado a: Para objetos trivialmente móviles , std::move
no tiene impacto.
Ahora veamos este ejemplo:
struct X
{
X& operator=(const X&);
};
void
test(X& i, X& j)
{
i = j;
}
Esto genera:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSERKS_ ## TAILCALL
.cfi_endproc
Si se ejecuta __ZN1XaSERKS_
a través de c++filt
ella produce: X::operator=(X const&)
. No es sorpresa aquí. Ahora si la prueba se cambia a:
void
test(X& i, X& j)
{
i = std::move(j);
}
Entonces todavía no hay ningún cambio en el código objeto generado. std::move
no ha hecho nada más que emitir j
un valor r, y luego ese valor r se X
une al operador de asignación de copias de X
.
Ahora agreguemos un operador de asignación de movimiento a X
:
struct X
{
X& operator=(const X&);
X& operator=(X&&);
};
Ahora el código objeto hace el cambio:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSEOS_ ## TAILCALL
.cfi_endproc
Correr a __ZN1XaSEOS_
través c++filt
revela que X::operator=(X&&)
se está llamando en lugar de X::operator=(X const&)
.
¡Y eso es todo std::move
! Desaparece por completo en tiempo de ejecución. Su único impacto es en tiempo de compilación, donde podría alterar la sobrecarga que se llama.
std::move
realmente se mueve ...