Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the copy constructor called instead of the move constructor when returning?

Let’s say I have the class MyClass with a correct move constructor and whose copy constructor is deleted. Now I am returning this class like this:

MyClass func()
{
    return MyClass();
}

In this case the move constructor gets called when returning the class object and everything works as expected.

Now let’s say MyClass has an implementation of the << operator:

MyClass& operator<<(MyClass& target, const int& source)
{
    target.add(source);
    return target;
}

When I change the code above:

MyClass func()
{
    return MyClass() << 5;
}

I get the compiler error, that the copy constructor cannot be accessed because it is deleted. But why is the copy constructor being used at all in this case?

like image 715
RomCoo Avatar asked Jun 25 '19 10:06

RomCoo


People also ask

What is the difference between a move constructor and a copy constructor?

If any constructor is being called, it means a new object is being created in memory. So, the only difference between a copy constructor and a move constructor is whether the source object that is passed to the constructor will have its member fields copied or moved into the new object.

Why is copy constructor called?

A copy constructor is called when a new object is created from an existing object, as a copy of the existing object. The assignment operator is called when an already initialized object is assigned a new value from another existing object.

Is copy constructor called on return?

The copy constructor is called because you call by value not by reference. Therefore a new object must be instantiated from your current object since all members of the object should have the same value in the returned instance.

Why copy constructor is called when we pass an object as an argument?

Because passing by value to a function means the function has its own copy of the object. To this end, the copy constructor is called.


2 Answers

Now I am returning this class via lvalue like this:

MyClass func()
{
    return MyClass();
}

No, the returned expression is an xvalue (a kind of rvalue), used to initialise the result for return-by-value (things are a little more complicated since C++17, but this is still the gist of it; besides, you're on C++11).

In this case the move constructor gets called when returning the class object and everything works as expected.

Indeed; an rvalue will initialise an rvalue reference and thus the whole thing can match move constructors.

When I change the code above:

… now the expression is MyClass() << 5, which has type MyClass&. This is never an rvalue. It's an lvalue. It's an expression that refers to an existing object.

So, without an explicit std::move, that'll be used to copy-initialise the result. And, since your copy constructor is deleted, that can't work.


I'm surprised the example compiles at all, since a temporary can't be used to initialise an lvalue reference (your operator's first argument), though some toolchains (MSVS) are known to accept this as an extension.


then would return std::move(MyClass() << 5); work?

Yes, I believe so.

However that is very strange to look at, and makes the reader double-check to ensure there are no dangling references. This suggests there's a better way to accomplish this that results in clearer code:

MyClass func()
{
    MyClass m;
    m << 5;
    return m;
}

Now you're still getting a move (because that's a special rule when returning local variables) without any strange antics. And, as a bonus, the << call is completely standard-compliant.

like image 136
Lightness Races in Orbit Avatar answered Oct 19 '22 10:10

Lightness Races in Orbit


Your operator return by MyClass&. So you are returning an lvalue, not an rvalue that can be moved automatically.

You can avoid the copy by relying on the standard guarantees regarding NRVO.

MyClass func()
{
    MyClass m;
    m << 5;
    return m;
}

This will either elide the object entirely, or move it. All on account of it being a function local object.


Another option, seeing as you are trying to call operator<< on an rvalue, is to supply an overload dealing in rvalue references.

MyClass&& operator<<(MyClass&& target, int i) {
    target << i; // Reuse the operator you have, here target is an lvalue
    return std::move(target);
}

That will make MyClass() << 5 itself well formed (see the other answer for why it isn't), and return an xvalue from which the return object may be constructed. Though such and overload for operator<< is not commonly seen.

like image 8
StoryTeller - Unslander Monica Avatar answered Oct 19 '22 10:10

StoryTeller - Unslander Monica