Question

I've been trying to teach myself the correct use of move semantics in C++11 through Bjarne Stroustrup's wonderful C++ book. I've encountered a problem - the move constructor is not being called as I expect it to be. Take the following code:

class Test
{
public:
    Test() = delete;
    Test(const Test& other) = delete;
    Test(const int value) : x(value) { std::cout << "x: " << x << " normal constructor" << std::endl; }
    Test(Test&& other) { x = other.x; other.x = 0; std::cout << "x: " << x << " move constructor" << std::endl; }

    Test& operator+(const Test& other) { x += other.x; return *this; }
    Test& operator=(const Test& other) = delete;
    Test& operator=(Test&& other) { x = other.x; other.x = 0; std::cout << "x :" << x << " move assignment" << std::endl; return *this; }

    int x;
};

Test getTest(const int value)
{
    return Test{ value };
}

int main()
{
    Test test = getTest(1) + getTest(2) + getTest(3);
}

This code will not compile - because I have deleted the default copy constructor. Adding the default copy constructor, the console output is as follows:

x: 3 normal constructor
x: 2 normal constructor
x: 1 normal constructor
x: 6 copy constructor

However, changing the main function to the following:

int main()
{
    Test test = std::move(getTest(1) + getTest(2) + getTest(3));
}

Produces the desired console output:

x: 3 normal constructor
x: 2 normal constructor
x: 1 normal constructor
x: 6 move constructor

This confuses me, because as far as I understand it, the result of (getTest(1) + getTest(2) + getTest(3)) is an rvalue (because it has no name, thus, no way to be used after it is assigned to the variable test) so it should be constructed using the move constructor by default, rather than requiring an explicit call to std::move().

Could somebody explain why this behaviour is occurring? Have I done something wrong? Do I just misunderstand the basics of move semantics?

Thank you.

EDIT 1:

I updated the code to reflect some of the comments below.

Added in class definition:

friend Test operator+(const Test& a, const Test& b) { Test temp = Test{ a.x }; temp += b; std::cout << a.x << " + " << b.x << std::endl; return temp; }
Test& operator+=(const Test& other) { x += other.x; return *this; }

Changed main to:

int main()
{
    Test test = getTest(1) + getTest(2) + getTest(4) + getTest(8);
}

This produces console output:

x: 8 normal constructor
x: 4 normal constructor
x: 2 normal constructor
x: 1 normal constructor
x: 1 normal constructor
1 + 2
x: 3 move constructor
x: 3 normal constructor
3 + 4
x: 7 move constructor
x: 7 normal constructor
7 + 8
x: 15 move constructor

Which I believe is what should be happening in this case - there is a lot of new object creation going on here, but on closer thought that makes sense, because each time operator+ is called, a temporary object must be created.

Interestingly, if I compile the revised code in release mode, the move constructor is never called, but in debug mode, it is called as the console output above describes.

EDIT 2:

Refining it even further. Add to class definition:

friend Test&& operator+(Test&& a, Test&& b) { b.x += a.x; a.x = 0; return std::move(b); }

Produces the console output:

x: 8 normal constructor
x: 4 normal constructor
x: 2 normal constructor
x: 1 normal constructor
x: 15 move constructor

Which is precisely the desired output.

EDIT 3:

I believe it would be better to do the following. Edit in class definition:

friend Test&& operator+(Test&& a, Test&& b) { b += a; return std::move(b); }
Test& operator+=(const Test& other) { std::cout << x << " += " << other.x << std::endl; x += other.x; return *this; }

This produces the console output:

x: 8 normal constructor
x: 4 normal constructor
x: 2 normal constructor
x: 1 normal constructor
2 += 1
4 += 3
8 += 7
x: 15 move constructor

Which is more descriptive. By implementing the rvalue operator+, a new object is not created for every single use of operator+, meaning that long chains of operator+ will have significantly better performance.

I think properly understand this lvalue/rvalue/move semantics magic now.

Was it helpful?

Solution

The result of getTest(1) + getTest(2) + getTest(3) has the same type as the return type of Test::operator+(const Test&). It's Test&, and therefore an lvalue.

operator + is typically a non-member overload that returns a temporary by value:

Test operator + (const Test& a, const Test& b)

or

Test operator + (Test a, const Test& b)

Bonus points for implementing operator += as a member, and using it in the implementation of the non-member operator+.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top