Move-semantics vs const reference [duplicate]
Asked Answered
E

4

7

my class has string variables and I want to initialize them with values passed to the constructor.

My teacher thought us to pass strings as a const-reference:

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

However Clang-Tidy suggest using the move command:

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

So I'm wondering what the correct way to do this in modern C++ is.

I already looked around, but nothing really answered my question. Thanks in advance!

Extenuatory answered 18/8, 2020 at 10:40 Comment(5)
In general, yes, second version is current standard. It may sometimes avoid a copy, while second always will perform a copy.Testator
Does this answer your question? Const reference VS move semanticsSay
If C++ had COW for std::string, then there would be a good case for the first.Quintal
For a situation like this, where the parameter is a data sink, I'd use MyClass::MyClass(std::string title_) : title{std::move(title_)} {}.Tartrate
@Testator Quibble: I'd call it "best practice" rather than "standard", also given the caveat in the accepted answer.Hak
S
7

None is optimal since they both default construct title first and then copy assign or move assign it. Use the member initializer list.

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

Let's look at them and see what happens in C++17:


#1 - A single converting constructor taking a const&.

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

This will create 1 or 2 std::strings in one of these ways:

  • The member is copy constructed.
  • A std::string is constructed by a std::string converting constructor and then the member is copy constructed.

#2 - A single converting constructor taking a std::string by value.

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

This will create 1 or 2 std::strings in one of these ways:

  • The argument is constructed by return value optimization from a temporary (str1 + str2) and then the member is move constructed.
  • The argument is copy constructed and then the member is move constructed.
  • The argument is move constructed and then the member is move constructed.
  • The argument is constructed by a std::string converting constructor and then the member is move constructed.

#3 - Combining two converting constructors.

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

This will create 1 or 2 std::strings in one of these ways:

  • The member is copy constructed.
  • The member is move constructed.
  • A std::string is constructed by a std::string converting constructor and then the member is move constructed.

So far, option #3 seems to be the most efficient option. Let's check a few options more.


#4 - Like #3 but replacing the moving conversion constructor with a forwarding constructor.

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

This will always create 1 std::string in one of these ways:

  • The member is copy constructed via A.
  • The member is move constructed via B.
  • The member is constructed by a std::string (possibly converting) constructor via B.

#5 - A forwarding constructor only - removing the copying conversion constructor from #4.

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

This will always create 1 std::string like in #4, but all is done via the forwarding constructor.

  • The member is copy constructed.
  • The member is move constructed.
  • The member is constructed by a std::string (possibly converting) constructor.

#6 - A single argument forwarding conversion constructor.

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

This will always create 1 std::string like in #4 and #5 but will only take one argument and forward it to the std::string constructor.

  • The member is copy constructed.
  • The member is move constructed.
  • The member is constructed by a std::string converting constructor.

Option #6 can easily be used to do perfect forwarding if you want to take multiple arguments in the MyClass constructor. Let's say you have an int member and another std::string member:

template<typename T, typename U>
MyClass(int X, T&& title, U&& title2) :
    x(X),
    title(std::forward<T>(title)),
    title2(std::forward<U>(title2))
{}
Sabrina answered 18/8, 2020 at 12:32 Comment(5)
Awesome. Follow-up question about a similar scenario: Is it possible to modify #4 so that I can pass multiple strings? And what if I wanted to pass multiple strings and an integer?Extenuatory
@Extenuatory I added option #6 for that - and I noticed that I forgot forwarding references in #5 so I rewrote that option now. :)Sabrina
Hm. I tried #6 now and CLion says everything is correct, but when I compile it, I get an error. It can't find the constructor. (Undefined reference). Do I need multiple constructors like in #4?Extenuatory
@Extenuatory example - You didn't try to put the implementation of the member function template in your .cpp file, did you? The member function template should be in the .hpp file.Sabrina
Yeah, that's exactly what I tried.. Putting everything in the header file worked. Thank you.Extenuatory
A
1

Copying a reference creates a copy of the original variable (original and the new one are on different areas), moving a local variable casts to a rvalue your local variable (and again, original and the new one are on different areas).

From the compiler point of view, move may be (and is) faster:

#include <string>

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

translates to:

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

However,

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

generates a bigger code (similar for 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"

So yes, std::move is better (under these circumstances).

Agenesis answered 18/8, 2020 at 11:10 Comment(0)
H
0

It' ok use a const reference, then use member initializer lists:

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

Where m_title is your member string in the class.

You can find useful help here: Constructor member initializer lists

Hochstetler answered 18/8, 2020 at 11:19 Comment(0)
M
0

there are 2 cases: lvalue or rvalue of std::string.

in std::string const& version, lvalue case is efficient enough, passed by reference and then copied. but a rvalue will be copied rather than moved, which is in much lower efficiency.

in std::string version, lvalue is copied when passed, and then moved to the member. rvalue will be moved twice in this case. but generally it's cheap, the move constructor.

besides, in std::string&& version, it can not receive a lvalue, but rvalue is passed by reference and then moved, better than moved twice.

so obviously, it's the best practice with both const& and &&, as what STL always does. but if move constructor is cheap enough, just passing by value and moving is also acceptable.

Minutia answered 18/8, 2020 at 11:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.