c++ c++11 move-semantics rvalue-reference c++-faq

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.