Move-semantics vs const reference [duplicato]
la mia classe ha variabili stringa e voglio inizializzarle con valori passati al costruttore.
Il mio insegnante ha pensato di passare le stringhe come riferimento const:
MyClass::MyClass(const std::string &title){
this->title = title
}
Tuttavia Clang-Tidy suggerisce di usare il comando move:
MyClass::MyClass(std::string title){
this->title = std::move(title)
}
Quindi mi chiedo quale sia il modo corretto per farlo nel moderno C++.
Mi sono già guardato intorno, ma niente ha davvero risposto alla mia domanda. Grazie in anticipo!
Risposte
Nessuno è ottimale poiché entrambi costruiscono title
prima di default e poi copiano o spostano l'assegnazione . Utilizzare l'elenco di inizializzatori di membri.
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
Diamo un'occhiata a loro e vediamo cosa succede in C++ 17:
# 1 - Un singolo costruttore di conversione che prende un file const&
.
MyClass::MyClass(const std::string& title) : title(title) {}
Questo creerà 1 o 2 std::string
s in uno di questi modi:
- Il membro è copiato.
- A
std::string
viene costruito da unstd::string
costruttore di conversione e quindi il membro viene costruito per copia.
#2 - Un singolo costruttore di conversione che prende a std::string
per valore.
MyClass(std::string title) : title(std::move(title)) {}
Questo creerà 1 o 2 std::string
s in uno di questi modi:
- L'argomento viene costruito dall'ottimizzazione del valore restituito da un temporaneo (
str1
+str2
) e quindi il membro viene costruito con lo spostamento. - L'argomento viene costruito per copia e quindi il membro viene costruito per spostamento.
- L'argomento viene costruito con il movimento e quindi il membro viene costruito con il movimento.
- L'argomento viene costruito da un
std::string
costruttore di conversione e quindi il membro viene costruito con lo spostamento.
#3 - Combinazione di due costruttori di conversione.
MyClass(const std::string& title) : title(title) {}
MyClass(std::string&& title) : title(std::move(title)) {}
Questo creerà 1 o 2 std::string
s in uno di questi modi:
- Il membro è copiato.
- Il membro viene costruito con movimento.
- A
std::string
viene costruito da unstd::string
costruttore di conversione e quindi il membro viene costruito con lo spostamento.
Finora, l'opzione #3
sembra essere l'opzione più efficiente. Controlliamo alcune opzioni in più.
# 4 - Come # 3 ma sostituendo il costruttore di conversione mobile con un costruttore di inoltro.
MyClass(const std::string& title) : title(title) {} // A
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {} // B
Questo creerà sempre 1 std::string
in uno di questi modi:
- Il membro è copiato tramite
A
. - Il membro è spostato costruito tramite
B
. - Il membro è costruito da un costruttore
std::string
(possibilmente convertito) tramiteB
.
# 5 - Solo un costruttore di inoltro - rimuovendo il costruttore di conversione di copia da # 4.
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {}
Questo creerà sempre 1 std::string
come in #4, ma tutto viene fatto tramite il costruttore di inoltro.
- Il membro è copiato.
- Il membro viene costruito con movimento.
- Il membro è costruito da un costruttore
std::string
(possibilmente di conversione).
# 6 - Un costruttore di conversione di inoltro a singolo argomento.
template<typename T>
explicit MyClass(T&& title) : title(std::forward<T>(title)) {}
Questo creerà sempre 1 std::string
come in #4 e #5 ma prenderà solo un argomento e lo inoltrerà al std::string
costruttore.
- Il membro è copiato.
- Il membro viene costruito con movimento.
- Il membro viene costruito da un
std::string
costruttore di conversione.
L'opzione #6
può essere facilmente utilizzata per eseguire un inoltro perfetto se si desidera accettare più argomenti nel MyClass
costruttore. Supponiamo che tu abbia un int
membro e un altro std::string
membro:
template<typename T, typename U>
MyClass(int X, T&& title, U&& title2) :
x(X),
title(std::forward<T>(title)),
title2(std::forward<U>(title2))
{}
La copia di un riferimento crea una copia della variabile originale (l'originale e quella nuova si trovano su aree diverse), lo spostamento di una variabile locale esegue il cast su un rvalue della variabile locale (e ancora, l'originale e quella nuova si trovano su aree diverse).
Dal punto di vista del compilatore, move
può essere (ed è) più veloce:
#include <string>
void MyClass(std::string title){
std::string title2 = std::move(title);
}
si traduce in:
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
Tuttavia,
void MyClass(std::string& title){
std::string title = title;
}
genera un codice più grande (simile 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"
Quindi sì, std::move
è meglio (in queste circostanze).
Va bene usare un riferimento const, quindi utilizzare gli elenchi di inizializzatori di membri:
MyClass(const std::string &title) : m_title{title}
Dove m_title è la tua stringa membro nella classe.
Puoi trovare una guida utile qui: Elenchi di inizializzatori di membri del costruttore
ci sono 2 casi: lvalue o rvalue di std::string
.
nella std::string const&
versione, lvalue case è abbastanza efficiente, passato per riferimento e quindi copiato . ma un rvalue verrà copiato anziché spostato , il che ha un'efficienza molto inferiore.
in std::string
version, lvalue viene copiato quando viene passato e quindi spostato nel membro. rvalue verrà spostato due volte in questo caso. ma generalmente è economico , il costruttore di mosse.
inoltre, in std::string&&
versione, non può ricevere un lvalue , ma rvalue viene passato per riferimento e poi spostato , meglio che spostato due volte.
quindi ovviamente, è la migliore pratica con entrambi const&
e &&
, come fa sempre STL. ma se il costruttore di mosse è abbastanza economico, è accettabile anche solo passare per valore e spostarsi.