10

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?

Peter Mortensen
  • 28,342
  • 21
  • 95
  • 123
RomCoo
  • 1,809
  • 2
  • 19
  • 33

3 Answers3

15

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.

Lightness Races in Orbit
  • 358,771
  • 68
  • 593
  • 989
8

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.

StoryTeller - Unslander Monica
  • 148,497
  • 21
  • 320
  • 399
1

Your operator<< takes its first parameter as a non-const reference. You can't bind a non-const reference to a temporary. But MyClass() returns the newly-created instance as a temporary.

Also, while func returns a value, operator<< returns a reference. So what else can it do but make a copy to return?

David Schwartz
  • 166,415
  • 16
  • 184
  • 259