Move-semantics vs const referencia [duplicado]

Aug 18 2020

mi clase tiene variables de cadena y quiero inicializarlas con valores pasados ​​​​al constructor.

Mi maestro pensó que pasáramos cadenas como referencia constante:

MyClass::MyClass(const std::string &title){
  this->title = title
}

Sin embargo, Clang-Tidy sugiere usar el comando mover:

MyClass::MyClass(std::string title){
  this->title = std::move(title)
}

Así que me pregunto cuál es la forma correcta de hacer esto en C++ moderno.

Ya miré a mi alrededor, pero nada realmente respondió a mi pregunta. ¡Gracias por adelantado!

Respuestas

3 TedLyngmo Aug 18 2020 at 19:32

Ninguno es óptimo, ya que ambos construyen titleprimero por defecto y luego copian, asignan o mueven y asignan . Utilice la lista de inicializadores de miembros.

MyClass::MyClass(const std::string& title) : title(title) {}         // #1
// or
MyClass::MyClass(std::string title) : title(std::move(title)) {}     // #2
//or
MyClass::MyClass(const std::string& title) : title(title) {}         // #3
MyClass::MyClass(std::string&& title) : title(std::move(title)) {}   // #3

Veámoslos y veamos qué sucede en C++17:


#1 - Un único constructor de conversión que toma un const&.

MyClass::MyClass(const std::string& title) : title(title) {}

Esto creará 1 o 2 std::strings de una de estas maneras:

  • El miembro es una copia construida.
  • A std::stringes construido por un std::stringconstructor de conversión y luego el miembro es copia construido.

#2 - Un solo constructor de conversión tomando un std::stringvalor por.

MyClass(std::string title) : title(std::move(title)) {}

Esto creará 1 o 2 std::strings de una de estas maneras:

  • El argumento se construye mediante la optimización del valor devueltostr1 a partir de un ( + ) temporal str2y, a continuación, se construye el movimiento del miembro.
  • El argumento se construye con una copia y luego el miembro se construye con un movimiento.
  • El argumento se construye en movimiento y luego el miembro se construye en movimiento.
  • El argumento se construye mediante un std::stringconstructor de conversión y luego el miembro se mueve.

#3 - Combinando dos constructores de conversión.

MyClass(const std::string& title) : title(title) {}
MyClass(std::string&& title) : title(std::move(title)) {}

Esto creará 1 o 2 std::strings de una de estas maneras:

  • El miembro es una copia construida.
  • El miembro se mueve construido.
  • A std::stringse construye mediante un std::stringconstructor de conversión y luego se mueve el miembro.

Hasta ahora, la opción #3parece ser la opción más eficiente. Veamos algunas opciones más.


#4 - Como #3 pero reemplazando el constructor de conversión móvil con un constructor de reenvío.

MyClass(const std::string& title) : title(title) {}                       // A
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {}  // B

Esto siempre creará 1 std::stringde una de estas maneras:

  • El miembro es una copia construida a través de A.
  • El miembro se mueve construido a través de B.
  • El miembro está construido por un constructor std::string(posiblemente de conversión) a través de B.

#5 - Solo un constructor de reenvío: eliminando el constructor de conversión de copia del #4.

template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {}

Esto siempre creará 1 std::stringcomo en el n. ° 4, pero todo se hace a través del constructor de reenvío.

  • El miembro es una copia construida.
  • El miembro se mueve construido.
  • El miembro está construido por un constructor std::string(posiblemente de conversión).

#6 - Un constructor de conversión de reenvío de un solo argumento.

template<typename T>
explicit MyClass(T&& title) : title(std::forward<T>(title)) {}

Esto siempre creará 1 std::stringcomo en el n. ° 4 y el n. ° 5, pero solo tomará un argumento y lo enviará al std::stringconstructor.

  • El miembro es una copia construida.
  • El miembro se mueve construido.
  • El miembro se construye mediante un std::stringconstructor de conversión.

La opción #6se puede usar fácilmente para hacer un reenvío perfecto si desea tomar múltiples argumentos en el MyClassconstructor. Digamos que tienes un intmiembro y otro std::stringmiembro:

template<typename T, typename U>
MyClass(int X, T&& title, U&& title2) :
    x(X),
    title(std::forward<T>(title)),
    title2(std::forward<U>(title2))
{}
1 Jose Aug 18 2020 at 18:10

Copiar una referencia crea una copia de la variable original (la original y la nueva están en áreas diferentes), mover una variable local convierte a un valor r en su variable local (y nuevamente, la original y la nueva están en áreas diferentes).

Desde el punto de vista del compilador, movepuede ser (y es) más rápido:

#include <string>

void MyClass(std::string title){
  std::string title2 = std::move(title);
}

se traduce a:

MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >): # @MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
        sub     rsp, 40
        mov     rax, rdi
        lea     rcx, [rsp + 24]
        mov     qword ptr [rsp + 8], rcx
        mov     rdi, qword ptr [rdi]
        lea     rdx, [rax + 16]
        cmp     rdi, rdx
        je      .LBB0_1
        mov     qword ptr [rsp + 8], rdi
        mov     rsi, qword ptr [rax + 16]
        mov     qword ptr [rsp + 24], rsi
        jmp     .LBB0_3
.LBB0_1:
        movups  xmm0, xmmword ptr [rdi]
        movups  xmmword ptr [rcx], xmm0
        mov     rdi, rcx
.LBB0_3:
        mov     rsi, qword ptr [rax + 8]
        mov     qword ptr [rsp + 16], rsi
        mov     qword ptr [rax], rdx
        mov     qword ptr [rax + 8], 0
        mov     byte ptr [rax + 16], 0
        cmp     rdi, rcx
        je      .LBB0_5
        call    operator delete(void*)
.LBB0_5:
        add     rsp, 40
        ret

Sin embargo,

void MyClass(std::string& title){
  std::string title = title;
}

genera un código más grande (similar a GCC):

MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&): # @MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)
        push    r15
        push    r14
        push    rbx
        sub     rsp, 48
        lea     r15, [rsp + 32]
        mov     qword ptr [rsp + 16], r15
        mov     r14, qword ptr [rdi]
        mov     rbx, qword ptr [rdi + 8]
        test    r14, r14
        jne     .LBB0_2
        test    rbx, rbx
        jne     .LBB0_11
.LBB0_2:
        mov     qword ptr [rsp + 8], rbx
        mov     rax, r15
        cmp     rbx, 16
        jb      .LBB0_4
        lea     rdi, [rsp + 16]
        lea     rsi, [rsp + 8]
        xor     edx, edx
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_create(unsigned long&, unsigned long)
        mov     qword ptr [rsp + 16], rax
        mov     rcx, qword ptr [rsp + 8]
        mov     qword ptr [rsp + 32], rcx
.LBB0_4:
        test    rbx, rbx
        je      .LBB0_8
        cmp     rbx, 1
        jne     .LBB0_7
        mov     cl, byte ptr [r14]
        mov     byte ptr [rax], cl
        jmp     .LBB0_8
.LBB0_7:
        mov     rdi, rax
        mov     rsi, r14
        mov     rdx, rbx
        call    memcpy
.LBB0_8:
        mov     rax, qword ptr [rsp + 8]
        mov     qword ptr [rsp + 24], rax
        mov     rcx, qword ptr [rsp + 16]
        mov     byte ptr [rcx + rax], 0
        mov     rdi, qword ptr [rsp + 16]
        cmp     rdi, r15
        je      .LBB0_10
        call    operator delete(void*)
.LBB0_10:
        add     rsp, 48
        pop     rbx
        pop     r14
        pop     r15
        ret
.LBB0_11:
        mov     edi, offset .L.str
        call    std::__throw_logic_error(char const*)
.L.str:
        .asciz  "basic_string::_M_construct null not valid"

Así que sí, std::movees mejor (bajo estas circunstancias).

abraxas Aug 18 2020 at 18:19

Está bien, use una referencia constante, luego use las listas de inicializadores de miembros:

MyClass(const std::string &title) : m_title{title}

Donde m_title es su cadena de miembro en la clase.

Puede encontrar ayuda útil aquí: Listas de inicializadores de miembros del constructor

RedFog Aug 18 2020 at 18:32

hay 2 casos: lvalue o rvalue de std::string.

en la std::string const&versión, el caso lvalue es lo suficientemente eficiente, se pasa por referencia y luego se copia . pero un rvalue se copiará en lugar de moverse , lo que tiene una eficiencia mucho menor.

en la std::stringversión, lvalue se copia cuando se pasa y luego se mueve al miembro. rvalue se moverá dos veces en este caso. pero generalmente es barato , el constructor de movimientos.

además, en std::string&&la versión, no puede recibir un lvalue , pero rvalue se pasa por referencia y luego se mueve , mejor que moverlo dos veces.

así que obviamente, es la mejor práctica con ambos const&y &&, como siempre lo hace STL. pero si move constructor es lo suficientemente barato, también es aceptable pasar por valor y mover.