c++ - ¿Es std::move(* this)un buen patrón?




c++11 move-semantics lvalue (2)

Sí, *this es siempre un valor l, no importa cómo se llame a una función miembro, por lo que si desea que el compilador lo trate como un valor r, debe usar std::move o equivalente. Tiene que ser, considerando esta clase:

struct A {
  void gun() &; // leaves object usable
  void gun() &&; // makes object unusable

  void fun() && {
    gun();
    gun();
  }
};

Si se hace *this un valor, esto sugiere que la primera llamada de la fun a la gun puede dejar el objeto inutilizable. La segunda llamada entonces fallaría, posiblemente mal. Esto no es algo que debería suceder implícitamente.

Esta es la misma razón por la que dentro de void f(T&& t) , t es un lvalue. A este respecto, *this no es diferente de cualquier parámetro de función de referencia.

Para hacer que este código con calificadores de referencia de C ++ 11 funcione como se espera, debo introducir un std::move(*this) que no suena bien.

#include<iostream>
struct A{
    void gun() const&{std::cout << "gun const&" << std::endl;}
    void gun() &&{std::cout << "gun&&" << std::endl;}
    void fun() const&{gun();}
    void fun() &&{std::move(*this).gun();} // <-- is this correct? or is there a better option
};

int main(){
    A a; a.fun(); // prints gun const&
    A().fun(); // prints gun&&
}

Algo no suena bien al respecto. ¿Es necesario el std::move ? ¿Es este un uso recomendado para ello? Por el momento, si no lo uso, obtengo un gun const& en ambos casos, lo que no es el resultado esperado.

(Parece que *this es una referencia implícita y lvalor siempre, lo que tiene sentido, pero la única forma de escapar es usar move )

Probado con clang 3.4 y gcc 4.8.3 .

EDITAR : Esto es lo que entiendo de @hvd respuesta:

1) std::move(*this) es sintácticamente y conceptualmente correcto

2) Sin embargo, si la gun no es parte de la interfaz deseada, no hay razón para sobrecargar las versiones lv-ref y rv-ref de la misma. Y dos funciones con nombres diferentes pueden hacer el mismo trabajo. Después de todo, los ref-calificadores son importantes en el nivel de interfaz, que generalmente es solo la parte pública.

struct A{
    private:
    void gun() const{std::cout << "gun const&" << std::endl;}
    void gun_rv(){std::cout << "gun called from fun&&" << std::endl;}
    public:
    void fun() const&{gun();}
    void fun() &&{gun_rv();} // no need for `std::move(*this)`.
};

Pero, de nuevo, si gun es parte de la interfaz (genérica), entonces std::move(*this) es necesario, pero solo entonces. Y también, incluso si la gun no es parte de la interfaz, existen ventajas de legibilidad al no dividir la función de la gun como una función con dos nombres diferentes y el costo de esta es, bueno ..., std::move(*this) .

EDIT 2 : En retrospectiva, esto es similar al caso C ++ 98 de sobrecarga const y no const de la misma función. En algunos casos , tiene sentido usar const_cast (otra forma de const_cast ) para no repetir el código y tener las dos funciones con el mismo nombre ... EDIT 3 : ... https://.com/a/124209/225186


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.







c++ c++11 this move-semantics lvalue