Hi guys, I have small problem with my example code.
Any hint?

#include <iostream>
using namespace std;

class A{
public:
    int a;

    A(int i){a = i;}

    A(const A& rhs) // copy constructor
    {
        a = 2;
    }

    A(const A&& rhs) // move constructor
    {
        a = 3;
    }

};

void print(A a)
{
    cout << a.a << endl;
}

A createA()
{
    A a(1);
    return a;
}

int main(){

    A a = createA();
    print(a);// This will invoker copy constructor and output is 2 

    print(createA()); // This should invoke move constructor because this object here is rvalue right?
}

Output is:
2
1

Sadly it should be:
2
3

Can't figure it out...

Recommended Answers

All 6 Replies

The way I understand it your move constructor is not correct. The intent of the move constructure is to move data objects from one class to another, not to mearly assign a value to the current class. See this article for full explanation.

Here is another good article from StackOverflow

I try to change data so I can see what function was invoked in process. Same as with cout. But whatever I do, I can't call move constructor. :v

This actually has nothing to do with the move constructor.

The reason why the move constructor is not called is because of return-value-optimization (RVO). This is one of the rare cases where the compiler is allowed to change the behavior for optimization purposes. The point here is that RVO allows the compiler (under some circumstances) to construct the return value of a function directly within the memory of the variable in which the result is to be stored in the call-site. So, the return value of the function createA() (called "a" internally) will be created directly in the same memory that the parameter "a" to the function print() will occupy. This allows the compiler to avoid an unnecessary copy (or move). This is not always possible, but it often is, and it certainly is happening in the trivial case in your example. So, that explains it.

Now, like I mentioned, there are cases where RVO cannot be applied. And so, we can use that knowledge to deliberately disable RVO by writing "stupid" code within the createA() function. Now, this could depend on how smart your compiler is and the level of optimizations enabled. Basically, the main requirement for RVO to be applied is that there should be a single return value in a function, this means, either you have a single named variable for the return value and all the return statements return that variable (this case is called "Named RVO" or NRVO), or you have a single return statement. In other words, here are a few examples:

A createA()
{
    A a(1);     // single return variable
    return a;   // all return statements return that variable
}               // --> NRVO applies here.

A createA()
{
    A a(1);
    if( 2 + 2 == 4 )
      return a;
    else
      return a; // all return statements return the same variable
}               // --> NRVO applies here.

A createA()
{
    return A(1); // only one return statement
}                // --> RVO applies here.

A createA()
{
    A a(1);
    if( 2 + 2 == 4 )
      return A(5);
    else
      return a;  // multiple return statements that are different
}                // --> RVO DOES NOT apply here.

So, you see, when there are multiple return variable (such as when you have multiple return statements that are different), the compiler cannot really construct one unique return variable directly into the destination memory, because there could be multiple ones. The compiler could be clever enough in some cases to be able to analyse the branches or something like that and manage to apply RVO where it would seem (at first glance) not to be possible, but that is not a guarantee. This is important to know if you want to avoid pessimizing (opposite of optimizing) your code just because of some trivial issue like that (in general, obviously, you want RVO to apply! So, you help the compiler!).

Here, I disabled RVO on your createA() function as so:

A createA()
{
    A a(1);
    if( false )
        return A(1);
    else
        return a;
}

A clever compiler might figure out that if( false ) will never be true, and therefore, that return statement will never execute, but apparently not the compiler on ideone.com, where I tested the code here.

Once RVO is disabled, the code calls the move-constructor, as expected. and the output is 2-3, as expected.

Normally, a move-constructor is created with a non-const rvalue reference (i.e., with A&& not const A&&), because it makes very little sense to actually "move" an immutable object. In fact, the only real purpose that a const rvalue reference has is to prevent a const rvalue from binding to a const lvalue reference (const A&) when you want to make sure that the const-reference you are getting is actually pointing to something "permanent" (i.e., an lvalue).

If you want other thoughts on implementing move constructor, read my tutorial on C++11 RAII classes.

Awesome, I didn't know that. :)

I understand why the move constructor is called when a function returns a class, such as CreateA(), but why isn't the move constructor called when a class is passed by value to a function, such as foo(A a)? Aren't they the same?

Aren't they the same?

Not exactly, but there are definite similarities. When you return an object from a function, there are two contexts to look at: within the function (callee) and just outside of it (caller).

In the callee context, when return-value-optimization (RVO) does not apply, the compiler will always implicitly try to use the move-constructor to create the returned object from whatever you provide in the return-statement. This is because if what appears in the return-statement is not some sort of object of a wider scope (data member of the containing class, or global variable, or passed-by-reference parameter) then it must be either a temporary object or a local object, and either way, it will be immediately destroyed as the function returns, which makes it safe to be "moved-out" of the function.

In the caller context, the object that is returned by a function is a temporary object. Here, I'm talking about that moment just after the function returns and before the return value is assigned to something (e.g., to a local variable). At that instant, it's a temporary object, i.e., an rvalue, which will bind (preferrably) to an rvalue-reference, leading to the use of move-semantics (e.g., move-constructor or move-assignment).

However, in either or both of these contexts, the move-construction can be optimized away by RVO or NRVO. This was also the case with the copy-constructors in prior versions of C++, and it is also still the case for objects that currently don't have a move-constructor. So, for return values, move-semantics just makes things better whenever RVO doesn't apply.

When you are talking about a parameter passed by value, things go the other way, and are not quite the same. First, there is the caller context where the object is passed to the function, and then there's the callee context where the object is received. In general, whether the object is moved or copied when passed to the function is really only a matter of the caller context. If you pass something that can be moved to create the parameter, then it will, otherwise it won't. So, if you pass a temporary object (pure rvalue) or if you pass an rvalue-reference (e.g., from using std::move()), then the parameter (that the callee receives) will be created with the move-constructor. Otherwise, the compiler has to use the copy-constructor. However, there is, again, an opportunity for optimization here, which is the general Copy Elision optimization. Copy elision can occur whenever the compiler finds that it makes sense, and copy elision also applies to move-semantics (i.e., you could call it "move elision"). Copy elision is what explains the situation when passed-by-value parameters are not copied or moved, but simply constructed in-place.

The general point here is that even though move operations are usually cheaper (much cheaper) than copy operations, it is still desirable for the compiler to attempt to optimize them away. So, just consider that RVO and copy elision rules apply just the same for (and take precedence over) move-semantics.

I think that the best way to understand what happens with return values and parameters of a function, is to imagine that the function is inlined. So, take this for example:

ClassA doSomething(ClassB b, ClassC& c, ClassD d) {
  /* doSomething's code */
  return some_a_object;
};

int main() {
  // ... some stuff..

  ClassA a = doSomething(some_b, some_c, ClassD(342));

  // ...
};

If you do a kind of manual inlining of that function call, you get approximately this (if I had to be completely accurate, it would be a bit too complicated):

int main() {
  // ... some stuff..

  ClassA a;
  {
    ClassA result;
    ClassB b = some_b;
    ClassC& c = some_c;
    ClassD d = ClassD(342);

    /* doSomething's code */
    result = some_a_object;

    a = result;
  };

  // ...
};

So, once you look at that, you can see that the creation of the d object can clearly be "copy elided" because there is no need to create a temporary object ClassD(342) and immediately copy it into the d object, instead, you can just create d directly. Similarly, if the some_b object could be moved, it would be.

And as for return-value-optimization, you can clearly see how redundant it is to create the result variable just to assign it to the a variable later. This is why the compiler can just optimize it away, leading to this leaner and meaner code:

int main() {
  // ... some stuff..

  ClassA a;
  {
    ClassB b = some_b;
    ClassC& c = some_c;
    ClassD d(342);

    /* doSomething's code */
    a = some_a_object;
  };

  // ...
};

It's useful to picture the code like that, because it gives you a pretty accurate idea of how optimizable some of the code is. And actually, whether inlined or not, all of the parameter construction and return-value construction code is actually executed in the caller context, just like in that example, and so, this isn't a matter of inlining or not, all functions can be optimized in this way.

(note: the main inaccuracy in this is the ordering of the parameters (which goes according to the calling convention used), and the fact that the variables like a and result do not get default-constructed and then assigned, instead, they are left unconstructed until the point of assignment, at which point, they are constructed in-place (e.g., placement-new))

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.