Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Move-semantics vs const reference [duplicate]

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!

like image 531
Finn Avatar asked Aug 18 '20 10:08

Finn


People also ask

Does const reference make a copy?

Not just a copy; it is also a const copy. So you cannot modify it, invoke any non-const members from it, or pass it as a non-const parameter to any function. If you want a modifiable copy, lose the const decl on protos .

Can you move from lvalue reference?

Yes, that is surprising and unconventional. If you want to permit a move, the convention is to have pass by value. You can std::move from inside the function as appropriate, while the caller can std::move into the argument if they decide they want to forfeit ownership.

Does const reference extend lifetime?

You are not questioning why const references are allowed to bind to temporaries, but merely why they extend the lifetime of those temporaries. If the lifetime of the temporary returned by bar() were not extended, then any usage of a (exemplified by the line (1)) would lead to undefined behavior.

What are the differences between passing a parameter by reference and by constant reference?

The important difference is that when passing by const reference, no new object is created. In the function body, the parameter is effectively an alias for the object passed in. Because the reference is a const reference the function body cannot directly change the value of that object.

How do I implement move semantics on my own classes?

Implementing move semantics on your own classes is fairly simple. You’ll typically need to define a move constructor, and possibly a move assignment operator. The move constructor just transfers things in, and leaves the source object empty.

Do I need a GitHub account to use move semantics?

To get benefits from move semantics you need some kind of resource (e.g. pointers to dynamically-allocated objects, file descriptors, TCP sockets, I/O streams, running threads, etc.). The source code containing the MemoryBuffer example is here (under MoveSemantics ). You do not need a Github account to download it.

What is ‘move’ in C++?

These concepts are common to many languages, and are essential to passing and returning data in your program. As of C++11, you can think of ‘move’ as being a third alternative. It’s expanded the core features of the language, adding move constructors and move-assign operators to your class design arsenal.

When do you not have to use move() in C++?

There are situations where the compiler will attempt a move implicitly, meaning you don’t have to use std::move (). This can only happen if a non-const movable object is being passed, returned, or assigned by value, and is about to be destroyed automatically.


2 Answers

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))
{}
like image 192
Ted Lyngmo Avatar answered Oct 19 '22 06:10

Ted Lyngmo


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).

like image 33
Jose Avatar answered Oct 19 '22 06:10

Jose