Move-semantics vs const reference [dupliquer]
ma classe a des variables de chaîne et je veux les initialiser avec des valeurs passées au constructeur.
Mon professeur pensait que nous devions passer des chaînes comme référence const :
MyClass::MyClass(const std::string &title){
this->title = title
}
Cependant, Clang-Tidy suggère d'utiliser la commande move :
MyClass::MyClass(std::string title){
this->title = std::move(title)
}
Je me demande donc quelle est la bonne façon de procéder en C++ moderne.
J'ai déjà regardé autour de moi, mais rien n'a vraiment répondu à ma question. Merci d'avance!
Réponses
Aucun n'est optimal car ils construisent tous les deux par défaut en title
premier, puis copient, assignent ou déplacent l'assignent . Utilisez la liste des initialiseurs de membres.
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
Regardons-les et voyons ce qui se passe en C++17 :
#1 - Un seul constructeur de conversion prenant un const&
.
MyClass::MyClass(const std::string& title) : title(title) {}
Cela créera 1 ou 2 std::string
s de l'une des manières suivantes :
- Le membre est une copie construite.
- A
std::string
est construit par unstd::string
constructeur de conversion, puis le membre est construit par copie.
#2 - Un seul constructeur de conversion prenant une std::string
valeur par.
MyClass(std::string title) : title(std::move(title)) {}
Cela créera 1 ou 2 std::string
s de l'une des manières suivantes :
- L'argument est construit par optimisation de la valeur de retour à
str1
partir d'un ( + ) temporairestr2
, puis le membre est construit par déplacement. - L'argument est construit par copie, puis le membre est construit par déplacement.
- L'argument est construit par déplacement, puis le membre est construit par déplacement.
- L'argument est construit par un
std::string
constructeur de conversion, puis le membre est construit par déplacement.
#3 - Combinaison de deux constructeurs de conversion.
MyClass(const std::string& title) : title(title) {}
MyClass(std::string&& title) : title(std::move(title)) {}
Cela créera 1 ou 2 std::string
s de l'une des manières suivantes :
- Le membre est une copie construite.
- Le membre est construit en mouvement.
- A
std::string
est construit par unstd::string
constructeur de conversion, puis le membre est construit par déplacement.
Jusqu'à présent, l'option #3
semble être l'option la plus efficace. Vérifions encore quelques options.
#4 - Comme #3 mais en remplaçant le constructeur de conversion mobile par un constructeur de transfert.
MyClass(const std::string& title) : title(title) {} // A
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {} // B
Cela créera toujours 1 std::string
de l'une des manières suivantes :
- Le membre est une copie construite via
A
. - Le membre est construit par déplacement via
B
. - Le membre est construit par un constructeur
std::string
(éventuellement converti) viaB
.
#5 - Un constructeur de transfert uniquement - suppression du constructeur de conversion de copie de #4.
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {}
Cela créera toujours 1 std::string
comme dans #4, mais tout se fait via le constructeur de transfert.
- Le membre est une copie construite.
- Le membre est construit en mouvement.
- Le membre est construit par un constructeur
std::string
(éventuellement converti).
#6 - Un constructeur de conversion de transfert à argument unique.
template<typename T>
explicit MyClass(T&& title) : title(std::forward<T>(title)) {}
Cela créera toujours 1 std::string
comme dans #4 et #5 mais ne prendra qu'un seul argument et le transmettra au std::string
constructeur.
- Le membre est une copie construite.
- Le membre est construit en mouvement.
- Le membre est construit par un
std::string
constructeur de conversion.
L'option #6
peut facilement être utilisée pour effectuer une transmission parfaite si vous souhaitez prendre plusieurs arguments dans le MyClass
constructeur. Disons que vous avez un int
membre et un autre std::string
membre :
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 copie d'une référence crée une copie de la variable d'origine (l'original et le nouveau sont sur des zones différentes), le déplacement d'une variable locale convertit en une rvalue votre variable locale (et encore une fois, l'original et le nouveau sont sur des zones différentes).
Du point de vue du compilateur, move
peut être (et est) plus rapide :
#include <string>
void MyClass(std::string title){
std::string title2 = std::move(title);
}
Se traduit par:
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
Cependant,
void MyClass(std::string& title){
std::string title = title;
}
génère un code plus gros (similaire pour 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"
Alors oui, std::move
c'est mieux (dans ces circonstances).
C'est bon, utilisez une référence const, puis utilisez les listes d'initialisation de membres :
MyClass(const std::string &title) : m_title{title}
Où m_title est votre chaîne de membre dans la classe.
Vous pouvez trouver une aide utile ici : Listes d'initialisation des membres du constructeur
il y a 2 cas : lvalue ou rvalue de std::string
.
en std::string const&
version, la casse lvalue est assez efficace, passée par référence puis copiée . mais une rvalue sera copiée plutôt que déplacée , ce qui est beaucoup moins efficace.
dans la std::string
version, lvalue est copiée lorsqu'elle est transmise , puis déplacée vers le membre. rvalue sera déplacé deux fois dans ce cas. mais généralement c'est pas cher , le constructeur de déménagement.
de plus, en std::string&&
version, il ne peut pas recevoir de lvalue , mais rvalue est passé par référence puis déplacé , mieux que déplacé deux fois.
donc évidemment, c'est la meilleure pratique avec const&
et &&
, comme ce que STL fait toujours. mais si le constructeur de déplacement est suffisamment bon marché, il suffit de passer par valeur et de déplacer est également acceptable.