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


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

Author: TemplateRex, 2014-01-26

2 answers

Es correcto que std::move(x) es solo un molde para rvalue , más específicamente para un xvalue , a diferencia de un prvalue. Y también es cierto que tener un molde llamado move a veces confunde a la gente. Sin embargo, la intención de este nombramiento no es confundir, sino hacer que su código sea más legible.

La historia de move se remonta a la propuesta original de move en 2002. Este documento presenta primero la referencia rvalue y luego muestra cómo para 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);
}

Uno tiene que recordar que en este punto de la historia, lo único que "&&" podría significar eralógico y . Nadie estaba familiarizado con las referencias rvalue, ni de las implicaciones de lanzar un lvalue a un rvalue (mientras que no hacer una copia como static_cast<T>(t) haría). Así que los lectores de este código pensarían naturalmente:

Sé cómo se supone que funciona swap (copiar a temporal y luego intercambiar los valores), pero qué ¿es el propósito de esos feos moldes?!

Tenga en cuenta también que swap es realmente solo un sustituto para todo tipo de algoritmos de modificación de permutación. Esta discusión es mucho , mucho mayor que swap.

Entonces la propuesta introduce syntax sugar que reemplaza el static_cast<T&&> con algo más legible que transmite no el qué preciso, sino más bien el por qué:{[60]]}

template <class T>
void
swap(T& a, T& b)
{
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

Es decir, move es solo azúcar de sintaxis para static_cast<T&&>, y ahora el el código es bastante sugestivo en cuanto a por qué esos moldes están allí: para habilitar la semántica de movimiento!

Uno debe entender que en el contexto de la historia, pocas personas en este punto realmente entendieron la conexión íntima entre rvalues y la semántica de movimiento (aunque el documento trata de explicar eso también):{[60]]}

La semántica de movimiento entrará automáticamente en juego cuando se le dé rvalue argumento. Esto es perfectamente seguro porque mover recursos de un rvalue no puede ser notado por el el resto del programa (nadie más tiene una referencia al rvalue para detectar una diferencia ).

Si en ese momento swap se presentaba 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:{[60]]}

Pero ¿por qué estás echando a rvalue?


El punto principal:

Como era, usando move, nadie preguntó: {[60]]}

Pero ¿por qué estás ¿mudarte?


A medida que pasaban los años y se refinaba la propuesta, las nociones de lvalue y rvalue se refinaron en las categorías de valor que tenemos hoy: {[60]]}

Taxonomía

(imagen descaradamente robada de dirkgently )

Y así hoy, si queremos swap decir con precisión lo que está haciendo, en lugar de por qué, debería verse más como: {[60]]}

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 preguntarse 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 journeyman C++ debería saber que bajo el capó de move, nada más está pasando que un cast. Y el programador principiante de C++, al menos con move, será informado de que la intención es mover desde el rhs, en lugar de copiar desde el rhs, incluso si no entienden exactamente cómo lograr.

Además, si un programador desea esta funcionalidad bajo otro nombre, std::move no posee ningún monopolio sobre esta funcionalidad, y no hay magia de lenguaje no portable involucrada en su implementación. Por ejemplo, si uno quiere codificar set_value_category_to_xvalue, y usar eso 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);
}

Así que si estás tan inclinado, decora tu static_cast<T&&> como mejor te parezca, y tal vez termines desarrollar una nueva práctica recomendada (C++ está en constante evolución).

Entonces, ¿qué hace move en términos de código objeto generado?

Considere 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 el código objeto. Uno puede generalizar este resultado a: Para objetos trivialmente movibles , 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 se produce: X::operator=(X const&). No es ninguna sorpresa. 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 convertir j a un rvalue, y luego ese rvalue X se une al operador de asignación de copia de X.

Ahora vamos a agregar una asignación de movimiento operador 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

Corriendo __ZN1XaSEOS_ a través de c++filt revela que X::operator=(X&&) está siendo llamado en 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 a overload.

 154
Author: Howard Hinnant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-23 03:45:05

Permítanme dejar aquí una cita de la C++11 FAQ escrita por B. Stroustrup, que es una respuesta directa a la pregunta de OP:

Move(x) significa "puedes tratar a x como un rvalue". Tal vez tendría ha sido mejor si move () había sido llamada rval (), pero ahora move() ha se ha utilizado durante años.

Por cierto, me gustó mucho el FAQ - vale la pena leer.

 19
Author: podkova,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-01-26 10:07:01