Move-semantics vs const referencia [duplicado]
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
Ninguno es óptimo, ya que ambos construyen title
primero 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::string
s de una de estas maneras:
- El miembro es una copia construida.
- A
std::string
es construido por unstd::string
constructor de conversión y luego el miembro es copia construido.
#2 - Un solo constructor de conversión tomando un std::string
valor por.
MyClass(std::string title) : title(std::move(title)) {}
Esto creará 1 o 2 std::string
s de una de estas maneras:
- El argumento se construye mediante la optimización del valor devuelto
str1
a partir de un ( + ) temporalstr2
y, 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::string
constructor 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::string
s de una de estas maneras:
- El miembro es una copia construida.
- El miembro se mueve construido.
- A
std::string
se construye mediante unstd::string
constructor de conversión y luego se mueve el miembro.
Hasta ahora, la opción #3
parece 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::string
de 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 deB
.
#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::string
como 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::string
como en el n. ° 4 y el n. ° 5, pero solo tomará un argumento y lo enviará al std::string
constructor.
- El miembro es una copia construida.
- El miembro se mueve construido.
- El miembro se construye mediante un
std::string
constructor de conversión.
La opción #6
se puede usar fácilmente para hacer un reenvío perfecto si desea tomar múltiples argumentos en el MyClass
constructor. Digamos que tienes un int
miembro y otro std::string
miembro:
template<typename T, typename U>
MyClass(int X, T&& title, U&& title2) :
x(X),
title(std::forward<T>(title)),
title2(std::forward<U>(title2))
{}
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, move
puede 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::move
es mejor (bajo estas circunstancias).
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
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::string
versió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.