c++ - ¿Por qué `std:: move` se llama` std:: move`?
c++11 move-semantics (2)
La función C ++ 11 std::move(x)
realmente no mueve nada en absoluto. Es solo un elenco para el valor r. ¿Por qué se hizo esto? ¿No es esto engañoso?
Es correcto que std::move(x)
es solo un elenco para valorizar - más específicamente a un valor x , a diferencia de un prvalue . Y también es cierto que tener un elenco llamado move
veces confunde a las personas. Sin embargo, la intención de este nombramiento no es confundir, sino hacer que su código sea más legible.
La historia del move
se remonta a la propuesta de movimiento original en 2002 . Este documento primero presenta la referencia rvalue, y luego muestra cómo escribir un std::swap
más eficiente:
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 es lógico y . Nadie estaba familiarizado con las referencias rvalue, ni con las implicaciones de emitir un lvalue a un valor r (sin hacer una copia como lo static_cast<T>(t)
). Entonces, los lectores de este código pensarían naturalmente:
Sé cómo se supone que el
swap
funciona (copie temporalmente y luego intercambie los valores), pero ¿cuál es el propósito de esos feos modelos?
Tenga en cuenta también que el swap
es realmente solo un sustituto de todo tipo de algoritmos de modificación de la 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 exactamente qué , 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, solo se trata de azúcar de sintaxis para static_cast<T&&>
, y ahora el código es bastante sugestivo de por qué esos moldes están ahí: ¡para habilitar la semántica de movimientos!
Uno debe entender que en el contexto de la historia, pocas personas en este punto entendieron realmente la conexión íntima entre los valores y la semántica del movimiento (aunque el documento también trata de explicar eso):
La semántica de movimiento entrará en juego automáticamente cuando se le den argumentos de valor real. Esto es perfectamente seguro porque el resto del programa no puede notar el movimiento de los recursos desde un valor r ( nadie más tiene una referencia al valor r para detectar una diferencia ).
Si en el momento el swap
se presentó en su lugar, 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 lanzando a valor?
El punto principal:
Tal como estaba, al usar move
, nadie preguntó:
Pero, ¿por qué te estás moviendo?
A medida que pasaron los años y la propuesta se refinó, las nociones de lvalue y rvalue se refinaron en las categorías de valores que tenemos hoy en día:
(imagen robada desvergonzadamente de dirkgently )
Y entonces, hoy, si quisiéramos swap
para 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 C ++ debe saber que bajo el capó de la move
, nada más está sucediendo que un elenco. Y el programador principiante de C ++, al menos con move
, será informado de que la intención es pasar de los rhs, en lugar de copiar desde los 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);
}
Entonces, si lo desea, decore su static_cast<T&&>
mejor le parezca, y quizás termine desarrollando una nueva mejor práctica (C ++ evoluciona constantemente).
Entonces, ¿qué hace move
en términos de código objeto generado?
Considera esta 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 el código objeto. Uno puede generalizar este resultado a: Para objetos movibles trivialmente , 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 ejecuta __ZN1XaSERKS_
través de c++filt
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 cambio alguno en el código objeto generado. std::move
ha hecho más que lanzar j
a un valor r, y luego ese valor r se une al operador de asignación de copia de X
Ahora permitamos agregar un operador de asignación de movimiento a X
:
struct X
{
X& operator=(const X&);
X& operator=(X&&);
};
Ahora el código objeto cambia:
__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
Ejecutar __ZN1XaSEOS_
través de c++filt
revela que se c++filt
X::operator=(X&&)
lugar de X::operator=(X const&)
.
¡Y eso es todo lo que hay para std::move
! Desaparece completamente en tiempo de ejecución. Su único impacto es en tiempo de compilación, donde podría alterar lo que se llama la sobrecarga.
Permítanme dejar aquí una cita de las preguntas frecuentes de C ++ 11 escritas por B. Stroustrup, que es una respuesta directa a la pregunta de OP:
move (x) significa "puedes tratar a x como un valor r". Tal vez hubiera sido mejor si move () se hubiera llamado rval (), pero ahora move () se ha utilizado durante años.
Por cierto, realmente disfruté las preguntas frecuentes, vale la pena leerlas.