Move-semantics vs const reference [dupliquer]

Aug 18 2020

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

3 TedLyngmo Aug 18 2020 at 19:32

Aucun n'est optimal car ils construisent tous les deux par défaut en titlepremier, 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::strings de l'une des manières suivantes :

  • Le membre est une copie construite.
  • A std::stringest construit par un std::stringconstructeur de conversion, puis le membre est construit par copie.

#2 - Un seul constructeur de conversion prenant une std::stringvaleur par.

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

Cela créera 1 ou 2 std::strings de l'une des manières suivantes :

  • L'argument est construit par optimisation de la valeur de retour àstr1 partir d'un ( + ) temporaire str2, 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::stringconstructeur 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::strings de l'une des manières suivantes :

  • Le membre est une copie construite.
  • Le membre est construit en mouvement.
  • A std::stringest construit par un std::stringconstructeur de conversion, puis le membre est construit par déplacement.

Jusqu'à présent, l'option #3semble ê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::stringde 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) via B.

#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::stringcomme 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::stringcomme dans #4 et #5 mais ne prendra qu'un seul argument et le transmettra au std::stringconstructeur.

  • Le membre est une copie construite.
  • Le membre est construit en mouvement.
  • Le membre est construit par un std::stringconstructeur de conversion.

L'option #6peut facilement être utilisée pour effectuer une transmission parfaite si vous souhaitez prendre plusieurs arguments dans le MyClassconstructeur. Disons que vous avez un intmembre et un autre std::stringmembre :

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

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, movepeut ê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::movec'est mieux (dans ces circonstances).

abraxas Aug 18 2020 at 18:19

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

RedFog Aug 18 2020 at 18:32

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::stringversion, 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.