C++ move semantics

Hello all,

Do you, please, agree with the text below?

• We refer to traditional C++ references (the single-ampersand one) as lvalue references. Modern C++ has introduced a new type called rvalue reference, denoted by placing a double ampersand && after some type. Such rvalue reference lets you modify the value of a temporary object. Rvalue references pave the way for the implementation of move semantics, a technique which can significantly increase the performance of your applications.

• Copy constructor/assignment is called when there’s an lvalue in input, while move constructor/assignment is called when there’s an rvalue in input.


Then please take a look at the following program:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#include "td_lib_facilities.h"

using namespace std;

class myVec {

public:
    myVec() { cout << " Default constructor is called.\n"; } // Default constructor
    myVec(const int&); // Ordinary onstructor
    myVec(const myVec&);  // Copy constructor 
    myVec& operator=(const myVec&); // Copy assignment
    myVec(myVec&&);  // Move constructor
    myVec& operator=(myVec&&);  // Move assignment 

    ~myVec() { cout << " Destructor is called.\n"; delete [] elem; }  // Destructor - Release recources 

    const double& operator[](int i) const { return elem[i]; }
    double& operator[](int i) { return elem[i]; }

    const int size() const { return sz; }

private:
    double* elem = nullptr;
    int sz = 0;
};

//******************************************************
                               // Ordinary onstructor
myVec::myVec(const int& size) {
    cout << " Ordinary constructor is called.\n";
    elem = new double[size];
    sz = size;
}

//************************************************
                                        // Copy constructor 
myVec::myVec(const myVec& other) {
    cout << " Copy constructor is called.\n";
    if (other.elem != nullptr)
    {
        elem = new double[other.sz];  // Could throw "bad_allocate" but will be caught by try-catch
        copy(other.elem, other.elem + other.size(), elem);
        sz = other.sz;
    }

    else throw invalid_argument{"Invalid Argument\n"};
}

//**************************************************
                                          // Copy assignment
myVec& myVec::operator=(const myVec& other)
{
    cout << " Copy assignment is called.\n";

    if (this != &other)
    {
        if (other.elem == nullptr)
            throw invalid_argument{"Invalid Argument\n"};
       
        double* p = new double[other.sz];  // Could throw "bad_allocate" but will be caught by try-catch
        copy(other.elem, other.elem + other.size(), p);

        delete[] elem; // Deleting old elements
        elem = p;
        sz = other.sz;
    }

    return *this;
}

//************************************************************
                                                 // Move constructor
myVec::myVec(myVec&& a) : elem(a.elem), sz(a.sz)
{
    cout << " Move constructor is called.\n";
    a.elem = nullptr;
    a.sz = 0;
}

//*****************************************************
                                   // Move assignment
myVec& myVec::operator=(myVec&& a)
{
    cout << " Move assignment is called.\n";
    if (this != &a)
    {
        delete[] elem;
        elem = a.elem;
        sz = a.sz;
        a.elem = nullptr;
        a.sz = 0;
    }
    return *this;
}

//***********************************************
int main() try
{
    myVec v1(5), v2(5);
    myVec v3 = v2;
    cout << v3.size() << "\n";
    v3 = v1;
    myVec v4 = myVec(10);
    cout << v4.size() << "\n";

    cin.get();
    return 0;
}

catch (invalid_argument& e)
{
   cerr << e.what() << "\n";
   abort();
}
catch (bad_alloc& e)
{
    cerr << e.what() << "\n";
    abort();
}
catch (...)
{
    cerr << "Something went wrong\n";
    abort();
}


I've got two questions:
1- Is the code fine to you too?
2- On line 103 myVec v4 = myVec(10);, we have a temp object myVec(10), then why isn't the move constructor called, please?
Last edited on
C++ compilers usually do something called "copy elision." In C++17, this is guaranteed, but compilers implemented it before that as well. What this basically means is that, in line 103, a copy/move is not considered and the object v4 is directly constructed with the temporary using the regular constructor. This is an optimization because now you don't call the regular constructor (to construct the temporary) followed by the move constructor (to construct the moved-into object with the temporary), you can instead just call the regular constructor to create the object instead of creating a temporary only to move it.

In gcc (if you are compiling with a standard lower than C++17), you can control copy elision with the compiler flag -fno-elide-constructors.

You can force the move constructor to be called by explicitly calling std::move
1
2
3
4
5
6
7
8
9
10
11
12
int main()try
{
    myVec v1(5), v2(5);
    myVec v3 = v2;
    cout << v3.size() << "\n";
    v3 = v1;
    myVec v4 = std::move(myVec(10));
    cout << v4.size() << "\n";

    cin.get();
    return 0;
}


Example with -fno-elide-constructors flag set:
http://coliru.stacked-crooked.com/a/c17d2d2abd9660fa
Last edited on
Yes. I got it, thank you.
Copy elision eliminates calling the copy constructor when the object (used to copy from) is temporary, and the ordinary constructor will then be called to directly create the new object, as you said, right? So for a snippet like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() try
{
    myVec v1(5), v2(5);
    myVec v3 = v2;
    cout << v3.size() << "\n";
    v3 = v1;
    myVec v4 = myVec(10);
    cout << v4.size() << "\n";
    v3 = myVec(15);
    cout << v3.size() << "\n";

    cin.get();
    return 0;
}


On line 9, the move assignment is called because v3 has previously been created.
I also read about return value optimization on the Web. But I think we just must tune these optimizations out and focus on designing our classes correctly to do their job properly and also let the compiler do its job properly as well (in needed).

But except for using std::move(), will you offer a snippet in which the move constructor is called, please.
I also read about return value optimization on the Web. But I think we just must tune these optimizations out and focus on designing our classes correctly to do their job properly and also let the compiler do its job properly as well (in needed).


Return value optimization is the same thing as copy elision, except it is a special type of copy elision that applies to return values of a function.

But except for using std::move(), will you offer a snippet in which the move constructor is called, please.


cppreference has a good example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <string>
#include <iostream>
#include <iomanip>
#include <utility>
 
struct A
{
    std::string s;
    int k;
    A() : s("test"), k(-1) { }
    A(const A& o) : s(o.s), k(o.k) { std::cout << "move failed!\n"; }
    A(A&& o) noexcept :
           s(std::move(o.s)),       // explicit move of a member of class type
           k(std::exchange(o.k, 0)) // explicit move of a member of non-class type
    { std::cout << "MOVED!" << std::endl;}
};
 
A f(A a)
{
    return a;
}
 
struct B : A
{
    std::string s2;
    int n;
    // implicit move constructor B::(B&&)
    // calls A's move constructor
    // calls s2's move constructor
    // and makes a bitwise copy of n
};
 
struct C : B
{
    ~C() { } // destructor prevents implicit move constructor C::(C&&)
};
 
struct D : B
{
    D() { }
    ~D() { }          // destructor would prevent implicit move constructor D::(D&&)
    D(D&&) = default; // forces a move constructor anyway
};
 
int main()
{
    std::cout << "Trying to move A\n";
    A a1 = f(A()); // return by value move-constructs the target from the function parameter
    std::cout << "Before move, a1.s = " << std::quoted(a1.s) << " a1.k = " << a1.k << '\n';
    A a2 = std::move(a1); // move-constructs from xvalue
    std::cout << "After move, a1.s = " << std::quoted(a1.s) << " a1.k = " << a1.k << '\n';
 
    std::cout << "Trying to move B\n";
    B b1;
    std::cout << "Before move, b1.s = " << std::quoted(b1.s) << "\n";
    B b2 = std::move(b1); // calls implicit move constructor
    std::cout << "After move, b1.s = " << std::quoted(b1.s) << "\n";
 
    std::cout << "Trying to move C\n";
    C c1;
    C c2 = std::move(c1); // calls copy constructor
 
    std::cout << "Trying to move D\n";
    D d1;
    D d2 = std::move(d1);
}
Topic archived. No new replies allowed.