sjsam
sjsam

Reputation: 21965

A move constructor is not invoked as expected

My custom implemented Integer class is below :

class Integer
{
private:
    int * ptr_int_; // Resource
public:
    // Other ctors
    Integer(Integer &&); // Move constructor
    // dtor
}

The move constructor is implemented as below :

Integer::Integer(Integer && arg)
{
    std::cout << "Integer(Integer && arg) called\n";
    this->ptr_int_ = arg.ptr_int_; // Shallow Copy
    arg.ptr_int_ = nullptr;
}

In my driver, for the below call,

Integer obj2{ Integer{5}}; 

I expected a parameterized constructor(for the temporary object) and then move constructor to be invoked. However the move constructor wasn't invoked.

In disassembly, i got the stuff shown below :

Integer obj2{ Integer{5}}; 
001E1B04  push        4  
001E1B06  lea         ecx,[obj2]  
001E1B09  call        Integer::__autoclassinit2 (01E1320h)  
001E1B0E  mov         dword ptr [ebp-114h],5  
001E1B18  lea         eax,[ebp-114h]  
001E1B1E  push        eax  
001E1B1F  lea         ecx,[obj2]  ;; Is this copy elision(RVO) in action?
001E1B22  call        Integer::Integer (01E12FDh)  
001E1B27  mov         byte ptr [ebp-4],1

I guess, this is Return Value Optimization(RVO) in action. Am I right?

Since most compilers implement RVO, I shouldn't be doing

 Integer obj2{ std::move(Integer{5})}; 

Should I?

Upvotes: 1

Views: 99

Answers (2)

luk32
luk32

Reputation: 16090

This is a tricky one, because it technically changed in c++17. In c++11 it is a NRVO optimization, but in c++17 it's not even an optimization anymore.

  1. You should not expect a move c'tor, it's up to compiler.
  2. Since c++17 you cannot expect it, it cannot be called.

Relevant excerpt from cppreference:

Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible, as the language rules ensure that no copy/move operation takes place, even conceptually:

  • [...]

  • In the initialization of a variable, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type:

T x = T(T(f())); // only one call to default constructor of T, to initialize x

Note: the rule above does not specify an optimization: C++17 core language specification of prvalues and temporaries is fundamentally different from that of the earlier C++ revisions: there is no longer a temporary to copy/move from. Another way to describe C++17 mechanics is "unmaterialized value passing": prvalues are returned and used without ever materializing a temporary.

Emphasis is mine on the important part. Above paragraph is in place since c++17 and non existent in c++11.

Now, c++11:

Under the following circumstances, the compilers are permitted, but not required to omit the copy and move (since C++11) construction of class objects even if the copy/move (since C++11) constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. This is an optimization: even when it takes place and the copy/move (since C++11) constructor is not called, it still must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed:

  • [...]

  • In the initialization of an object, when the source object is a nameless temporary and is of the same class type (ignoring cv-qualification) as the target object. When the nameless temporary is the operand of a return statement, this variant of copy elision is known as RVO, "return value optimization". (until c++17)

This is your case. So for c++11 it is RVO for initialization. return statement RVO is actually covered by another bullet.

Upvotes: 2

JonatanE
JonatanE

Reputation: 941

The move constructor is not invoked due to RVO just as you figured out yourself.

Note that std::move simply casts its argument into an rvalue reference. In your example Integer{5} is an unnamed temporary and already an rvalue. The extra call to std::move is therefore unnecessary. The move constructor will be called anyway, if it is not completely elided as in your case.

Also, note that in your implementation of the move constructor the extra std::move is unnecessary since ptr_int_ is a raw pointer without any special move semantics.

Upvotes: 2

Related Questions