c++move-semanticsmove-constructorcopy-elision

Move Constructor vs Copy Elision. Which one gets called?


I have two pieces of code here to show you. They are two classes and each one provides a Move Constructor and a function which returns a temporary.

I'm confused: in both cases I define a Move Constructor and a random member function returning a temporary. But the behavior changes, and my question is why.

Note that in the following examples, the operator<< was overloaded in order to print a list (in the first case) and the double data member (in the second case).


MOVE CONSTRUCTOR GETS CALLED

template<typename T>
class GList
{
public:
    GList() : il{ nullptr } {}

    GList(const T& val) : il{ new Link<T>{ val,nullptr } }  {}

    GList(const GList<T>& copy) {}

    GList(GList<T>&& move)
    {
        std::cout << "[List] Move constructor called" << std::endl;

        // ... code ...
    }

    // HERE IS THE FUNCTION WHICH RETURNS A TEMPORARY!
    GList<T> Reverse()
    {
        GList<T> result;

        if (result.il == nullptr)
            return *this;

        ...
        ...
        ...

        return result;
    }
};

int main()
{

   GList<int> mylist(1);

   mylist.push_head(0);

   cout << mylist.Reverse();

   return 0;
}

The output is:

[List] Move constructor called

0

1


COPY ELISION PERFORMED

class Notemplate
{
   double d;
public:
   Notemplate(double val)
   {
      d = val;
   }

   Notemplate(Notemplate&& move)
   {
       cout << "Move Constructor" << endl;
   }

   Notemplate(const Notemplate& copy)
   {
       cout << "Copy" << endl;
   }

   Notemplate Redouble()
   {
       Notemplate example{ d*2 };
       return example;
   }
};

int main()
{
   Notemplate my{3.14};

   cout << my.Redouble();

   return 0;
}

The output is:

6.28


I was expecting a call to the Move Constructor in the second example. After all, the logic for the function is the same: return a temporary.

Will someone explain me why that's not happening?

How do I deal with copy elisions?

I want my code to be the most portable I can, how can I be sure of these kinds of optimizations by the compiler?


Solution

  • In the comments of another SO answer, the OP clarifies what he is asking here:

    I heard that copy elision CAN occur even when there are more than 1 return statements. I'd like to know when a copy elision is forbidden

    And so I am attempting to address this issue here:

    Elision of copy/move operations (referred to as copy elision by the C++ standard) is permitted in the following circumstances:

    Copy elision is forbidden in all other circumstances.

    The number of return statements in a function has no bearing whatsoever on the legality of copy elision. However a compiler is permitted to not perform copy elision, even though it is legal, for any reason at all, including the number of return statements.

    C++17 Update

    There are now a few places where copy elision is mandatory. If a prvalue can be bound directly to a by-value function parameter, or a by-value return type, or to a named local variable, copy elision is mandatory in C++17. This means that the compiler shall not bother even checking for a copy or move constructor. Legal C++17:

    struct X
    {
        X() = default;
        X(const X&) = delete;
        X& operator=(const X&) = delete;
    };
    
    X
    foo(X)
    {
        return X{};
    }
    
    int
    main()
    {
        X x = foo(X{});
    }