Move Constructor vs Copy Elision. Which one gets called?
Asked Answered
L

2

6

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.

  • In the first case, the function returning a temporary calls the Move Constructor
  • In the second case, the function returning a temporary just tells the compiler to perform a copy elision

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?

Loyalty answered 19/2, 2016 at 13:38 Comment(2)
What's the signature of two operator<<?Naif
friend std::ostream& operator<<(std::ostream& s, const ClassName& other)Loyalty
S
11

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:

  • In a return statement in a function with a class return type, when the expression is the name of anon-volatile object with automatic storage duration (other than a function parameter or a variable introduced by the exception-declaration of a handler) with the same type (ignoring cv-qualification) as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value.

  • In a throw-expression, when the operand is the name of a non-volatile automatic object (other than a function or catch-clause parameter) whose scope does not extend beyond the end of the innermost enclosing try-block (if there is one), the copy/move operation from the operand to the exception object can be omitted by constructing the automatic object directly into the exception object.

  • When a temporary class object that has not been bound to a reference would be copied/moved to a class object with the same type (ignoring cv-qualification), the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move.

  • When the exception-declaration of an exception handler declares an object of the same type (except for cv-qualification) as the exception object, the copy operation can be omitted by treating the exception-declaration as an alias for the exception object if the meaning of the program will be unchanged except for the execution of constructors and destructors for the object declared by the exception-declaration. There cannot be a move from the exception object because it is always an lvalue.

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{});
}
Spiro answered 18/3, 2016 at 22:8 Comment(3)
Thanks professor :) Is there a way to keep my code as portable as possible? A way to be independent from the choises of a compilerLoyalty
@gedamial: Yes. Don't put side effects in your special members (copy/move constructors) that don't pertain to copying/moving your objects (except perhaps for educational or debugging purposes). Examples of such side effects are print statements (as in your question), or counters (counting the number of copies or moves). If you do have such side effects, ensure that the correctness of your program does not depend on these side effects, so that if & when copy elision happens, the correctness of your program is not adversely impacted.Spiro
Thanks very very much sir. You were awesomeLoyalty
L
1

The copy elision is an optimization that, nowadays, every modern compiler provides.

When returning huge class objects in C++, this technique applies... but not in every case!

In the first example, the compiler performs the Move Constructor because we have more than one return statement in the function.

Loyalty answered 20/2, 2016 at 12:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.