0

I just picked up Effective Modern C++ by Scott Meyers at my local library. On page 4 of my copy, Meyers writes about the different types of copies: copy via move constructor and copy via copy constructor. The example given was the following code:

void someFunc(Foo f); 
Foo bar; 
someFunc(bar); 
someFunc(std::move(bar));

My question is how can the function someFunc accept a rvalue reference created by std::move when the argument type is not rvalue reference?

James Meas
  • 327
  • 4
  • 16

2 Answers2

4

The function expects a value - not a reference, so when a reference is passed as an argument, a copy of some object is being made.

When you pass an lvalue reference, the argument becomes a copy of the object binded with said reference - it is being copy constructed. You have an object, a reference to that object and a copy constructed argument that was created based on the said object.

When you pass an rvalue reference, is seems like not a lot of things change. You still have some object, some reference to it and another object that is being created based on the original one, but this time, the newly created object is being created via move constructor.

What is the difference? For primitive types, none. Moving and copying ints, chars and such does pretty much the same thing. The differences can be observed when we unit test the function with an object that actually uses move construction in some way. Let's maybe take a look at how std::string behaves:

void foo(std::string str)
{
    std::cout << str << " ";
}

int main()
{
    std::string first   = "abc";
    std::string& second = first;
    std::string third   = "ghi";

    foo(first);             // copy the first string and display it
    foo(second);            // copy the first string and display it
    foo(std::move(third));  // move the third string and display it
}

Given this example, the comments say very little. We already established that here we copy construct, there we move construct. But what does it actually mean?

Moving an object is not so trivial topic. I encourage you to read this answer to get some insights on it. Tl;dr-ing, when you move an std::string, you don't make an whole new copy - you create another object which steals the representation of the original and leaves it in valid, but unspecified state.

Concluding: Given the first two calls to foo(), the parameter being a value and arguments being values or references, we are ending up with a simple copy. Given the third call, we are providing an rvalue reference, thus we enforce a move construction (because arguments type is a value, not a reference, so it needs an actual object), which will take the third string, create an entirely new one inside foos call stack, steal it's internal pointer, leave third in some valid state (you might end up with third begin an empty std::string with 0 size, but that's unspecified) and display the moved string.

Fureeish
  • 12,533
  • 4
  • 32
  • 62
  • 1
    Thanks. The missing piece was constructor being called when you pass an object to a function that takes a non-reference. I figured it would fail because there is not a rvalue reference in parameters, but obviously this is wrong. – James Meas Jul 13 '18 at 03:46
1

In the example code:

void someFunc(Foo f); 
Foo bar; 
someFunc(bar); 
someFunc(std::move(bar));

The function someFunc doesn't even know how the call was made. It always receives the value f.


What happens at the call site depends on the constructors of Foo. There could be a separate move constructor:

class Foo
{
public:
   Foo();
   Foo(const Foo&);
   Foo(Foo&&);
};

And then the parameters would be handled by the different constructors.


But it could also be that Foo only has a copy constructor. Perhaps because moving doesn't improve anything for this class:

class Foo
{
public:
   Foo();
   Foo(const Foo&);
};

Then both calls would end up being identical.


It could even be that a (slightly perverse) class forbids moving by deleting the move constructor:

class Foo
{
public:
   Foo();
   Foo(const Foo&);
   Foo(Foo&&) = delete;
};

Then the second call will fail.


So - it depends!

Bo Persson
  • 90,663
  • 31
  • 146
  • 203