0

The following code would not compile:

#include <utility>

class Foo{
public:
    Foo(const Foo& foo) = delete; 
    explicit Foo(Foo&& foo) = default;
    Foo() = default;
    Foo bar(){ 
        Foo foo;
        return foo;
    }
};

with the compiler message of:

test_rand.cpp:10:16: error: use of deleted function ‘Foo::Foo(const Foo&)’
   10 |         return foo;
      |                ^~~
test_rand.cpp:5:5: note: declared here
    5 |     Foo(const Foo& foo) = delete;
      |     ^~~

Thinking that this is because the copy constructor is deleted, and when the function return a temporary variable needs to be created, I added std::move to make foo a rvalue so that the move constructor can be called.

#include <utility>

class Foo{
public:
    Foo(const Foo& foo) = delete; 
    explicit Foo(Foo&& foo) = default;
    Foo() = default;
    Foo bar(){ 
        Foo foo;
        return std::move(foo);
    }
};

However, the compiler gives me the exact same error "use of deleted function 'Foo::Foo(const Foo&)'."

I then tried to remove the explicit keyword for the move constructor, and everything worked, even without the std::move

I wonder what the internal mechanism is for this. Specifically, what are the detailed steps for the compiler to return that value with only a move constructor, and what implicit conversions happen in the return process?

With the explicit keyword kept, I also found that if I changed the return line to return Foo(std::move(foo)), the error disappeared. But what is the difference between this and return std::move(foo), considering both of them are rvalues. And if I want to keep the move constructor explicit, is there a better way of doing so?

6
  • Copy elision might perform the move for you or even get rid of the copy. Which C++ version are you using? Commented Jul 4, 2024 at 19:31
  • 1
    To make it explicit, you would need return Foo(std::move(foo));, wouldn't you? Commented Jul 4, 2024 at 19:32
  • 2
    "if I want to keep the move constructor explicit" > why would you want to do that? See also Commented Jul 4, 2024 at 19:37
  • @ThomasWeller Yeah, it worked after using this. However, I don't really understand the difference between this and std::move(foo), since they're all rvalues. Commented Jul 4, 2024 at 19:38
  • 2
    std::move(foo) will treat foo as an lvalue reference as it has a name and attempt to convert it to an rvalue. The explicit prevents the conversion. Commented Jul 4, 2024 at 19:51

3 Answers 3

4

The result object of a function call is initialized by copy-initialization from the operand of the return statement. That's the same initialization that you would have e.g. for a function parameter or for initialization with = initializer syntax.

If the operand is a xvalue, such as std::move(tmp), but in a return statement also just tmp, then copy-initialization will result in a call to the copy constructor, because copy-initialization generally does not consider explicit constructors, just the same as in

Foo a;
Foo b = std::move(a);

or

void f(Foo);    

Foo a;
f(std::move(a));

If however the return statement's operand is a prvalue such as Foo(std::move(tmp)), then copy-initialization means that the object will be initialized from the initializer of the prvalue. (So-called "mandatory copy elision".) The initialization of the prvalue Foo(std::move(tmp)) is direct-initialization. So the result object of the function call will be initialized by direct-initialization from the argument list (std::move(tmp)). That's the difference to earlier where it was copy-initialized from std::move(tmp).

In direct-initialization all constructors are considered against the argument list and so the explicit move constructor may be chosen. In this case std::move(tmp) is also required, because tmp is only automatically a xvalue in a return statement if it is the whole operand.

That's the same behavior as e.g. in

Foo a;
Foo b = Foo(std::move(a));

or

void f(Foo);    

Foo a;
f(Foo(std::move(a)));
Sign up to request clarification or add additional context in comments.

2 Comments

"If the operand is a xvalue, such as std::move(tmp), but in a return statement also just tmp" — AFAIK, this holds since C++23: timsong-cpp.github.io/cppwp/n4950/…. Before C++23, tmp is not xvalue, but there is a different mechanism for resolution of overloading instead: timsong-cpp.github.io/cppwp/n4868/class#copy.elision-3.
@DanielLangr Yes, correct. I simplified it because explaining the pre-C++23 rules in detail takes a while.
0

When you call bar(), Foo bar = baz.bar(), and it does return Foo(std::move(foo)), the copy elision makes it equivalent

Foo bar(std::move(foo));  // explicit move constructor.

This is fine. When Foo::bar() does return std::move(foo);, the copy elision does not apply and makes this

Foo tmp(std::move(foo));
Foo bar(tmp);  // copy constructor.

This can't compile do you the deleted copy constructor.

Comments

0

With the explicit keyword kept, I also found that if I changed the return line to return Foo(std::move(foo)), the error disappeared. But what is the difference between this and return std::move(foo), considering both of them are rvalues.

Both are rvalues, but what do you do with those rvalues? In the first case, you explicitly construct a Foo object from it (then return value optimization kicks in, avoiding the need to construct another object). In the second case, you merely say what to do with the rvalue (return it), with the implication that the returned object be constructed from it (without that implication, you would be returning Foo&& instead of Foo).

So, what you see is the consequence of marking a constructor explicit. An explicit constructor must be invoked explicitly.

You're also seeing why an explicit move constructor is awkward...

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.