¿Por qué `std :: move` se llama` std :: move`?

128

La función C ++ 11 std::move(x)realmente no mueve nada en absoluto. Es solo un reparto al valor r. ¿Por qué se hizo esto? ¿No es esto engañoso?

Howard Hinnant
fuente
Para empeorar las cosas, el argumento de tres std::moverealmente se mueve ...
Cubbi
Y no te olvides del C ++ std::char_traits::move
98/03/11
30
Mi otro favorito es el std::remove()que no elimina los elementos: aún debe llamar erase()para eliminar realmente esos elementos del contenedor. Entonces moveno se mueve, removeno se quita. Hubiera elegido el nombre mark_movable()para move.
Ali
44
@ Ali también me parecería mark_movable()confuso. Sugiere que hay un efecto secundario duradero donde de hecho no hay ninguno.
finnw

Respuestas:

178

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 moveveces 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 movese 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 swapse 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 swaprealidad 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, movees 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 swapse 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:

Taxonomia

(imagen robada descaradamente de dirkgently )

Y así, hoy, si quisiéramos swapdecir 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::moveno 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 moveen 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::moveno 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++filtella 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::moveno ha hecho nada más que emitir jun valor r, y luego ese valor r se Xune 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++filtrevela 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.

Howard Hinnant
fuente
77
Aquí hay una fuente de puntos para ese gráfico: lo recreé digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } }para el bien público :) Descárguelo aquí como SVG
sehe
77
¿Esto todavía está abierto a bikeshedding? Sugeriría allow_move;)
dyp
2
@dyp Mi favorito sigue siendo movable.
Daniel Frey
66
Scott Meyers sugirió cambiar el nombre std::movea rvalue_cast: youtube.com/…
nairware
66
Dado que rvalue ahora se refiere tanto a prvalues ​​como a xvalues, rvalue_castes ambiguo en su significado: ¿qué tipo de rvalue devuelve? xvalue_castSería un nombre consistente aquí. Desafortunadamente, la mayoría de las personas, en este momento, tampoco entenderían lo que está haciendo. En unos años más, espero que mi declaración se vuelva falsa.
Howard Hinnant
20

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 x como un valor r". Quizás hubiera sido mejor si move () se hubiera llamado rval (), pero ahora move () se ha usado durante años.

Por cierto, realmente disfruté las preguntas frecuentes: vale la pena leerlo.

podkova
fuente
2
Para plagiar el comentario de @ HowardHinnant de otra respuesta: la respuesta de Stroustrup es inexacta, ya que ahora hay dos tipos de valores: prvalues ​​y xvalues, y std :: move es realmente un elenco de xvalue.
einpoklum