Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why isn't move constructor elided whenever possible with `make_x()` functions?

Tags:

I cannot figure out why in the last case is the move constructor called when copy elision is enabled (or even mandatory such as in C++17):

class X {
  public:
    X(int i) { std::clog << "converting\n"; }
    X(const X &) { std::clog << "copy\n"; }
    X(X &&) { std::clog << "move\n"; }
};

template <typename T>
X make_X(T&& arg) {
  return X(std::forward<T>(arg));
}

int main() {
  auto x1 = make_X(1);    // 1x converting ctor invoked
  auto x2 = X(X(1));      // 1x converting ctor invoked
  auto x3 = make_X(X(1)); // 1x converting and 1x move ctor invoked
}

What rules hinder the move constructor to be elided in this case?

UPDATE

Maybe more straightforward cases when move constructors are called:

X x4 = std::forward<X>(X(1));
X x5 = static_cast<X&&>(X(1));
like image 446
Daniel Langr Avatar asked Jan 12 '18 12:01

Daniel Langr


People also ask

Is move constructor automatically generated?

No move constructor is automatically generated.

Does STD move move constructor?

std::move is actually just a request to move and if the type of the object has not a move constructor/assign-operator defined or generated the move operation will fall back to a copy.

What does implicit move constructor do?

Implicitly-defined move constructor For non-union class types (class and struct), the move constructor performs full member-wise move of the object's bases and non-static members, in their initialization order, using direct initialization with an xvalue argument.


2 Answers

The two cases are subtly different, and it's important to understand why. With the new value semantics in C++17, the basic idea is that we delay the process of turning prvalues into objects as long as possible.

template <typename T>
X make_X(T&& arg) {
  return X(std::forward<T>(arg));
}

int main() {
  auto x1 = make_X(1);
  auto x2 = X(X(1));
  auto x3 = make_X(X(1));
}

For x1, the first expression we have of type X is the one in the body of make_X, which is basically return X(1). That's a prvalue of type X. We're initializing the return object of make_X with that prvalue, and then make_X(1) is itself a prvalue of type X, so we're delaying the materialization. Initializing an object of type T from a prvalue of type T means directly initializing from the initializer, so auto x1 = make_X(1) reduces to just X x1(1).

For x2, the reduction is even simpler, we just directly apply the rule.

For x3, the scenario is different. We have a prvalue of type X earlier (the X(1) argument) and that prvalue binds to a reference! At the point of binding, we apply the temporary materialization conversion - which means we actually create a temporary object. That object is then moved into the return object, and we can do prvalue reduction on the subsequent expression all the way. So this reduces to basically:

X __tmp(1);
X x3(std::move(__tmp));

We still have one move, but only one (we can elide chained moves). It's the binding to a reference that necessitates the existence of a separate X object. The argument arg and the return object of make_X must be different objects - which means a move must happen.


For the last two cases:

X x4 = std::forward<X>(X(1));
X x5 = static_cast<X&&>(X(1));

In both cases, we're binding a reference to a prvalue, which again necessitates the temporary materialization conversion. And then in both cases, the initializer is an xvalue, so we don't get the prvalue reduction - we just have move construction from the xvalue that was a materialized temporary object from a prvalue.

like image 166
Barry Avatar answered Feb 20 '23 19:02

Barry


Because in the expression X(std::forward<T>(arg)), even if, in the last case, arg is a reference bound to a temporary, it is still not a temporary. Inside the function body, the compiler cannot ensure that arg is not bound to an lvalue. Consider what would happen if the move constructor was elided and you would perform this call:

auto x4 = make_X(std::move(x2));

x4 would become an alias for x2.

The rules for move elision of the return value is described in [class.copy]/32:

[...]This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function's return value

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

In the call make_X(X(1)) copy elision actualy happens, but only once:

  1. First X(1) creates a temporary that is bound to arg.
  2. Then X(std::forward<T>(arg)) invokes the move constructor. arg is not a temporary so the second rule above does not apply.
  3. Then the result the expression X(std::forward<T>(arg)) should also be moved to construct the return value but this move is elided.

About your UPDATE, std::forward cause materialisation of the temporary X(1) that is bound to an xvalue: the return of std::forward. This returned xvalue is not a temporary so copy/elision is not anymore applicable.

Again what would happen in this case if move elision occured. (The c++ grammar is not contextual):

auto x7 = std::forward<X>(std::move(x2));

Nota: After I have seen a new answer about C++17 I wanted to add to confusion.

In C++17, the definition of prvalue is that changed that there are not any more any move constructor to elide inside your example code. Here example of result code of GCC with the option fno-elide-constructors in C++14 and then in C++17:

#c++ -std=c++14 -fno-elide-constructors | #c++ -std=c++17 -fno-elide-constructors
main:                                   | main:
  sub rsp, 24                           |   sub rsp, 24
  mov esi, 1                            |   mov esi, 1
  lea rdi, [rsp+15]                     |   lea rdi, [rsp+12]
  call X::X(int)                        |   call X::X(int)
  lea rsi, [rsp+15]                     |   lea rdi, [rsp+13]
  lea rdi, [rsp+14]                     |   mov esi, 1
  call X::X(X&&)                        |   call X::X(int)
  lea rsi, [rsp+14]                     |   lea rdi, [rsp+15]
  lea rdi, [rsp+11]                     |   mov esi, 1
  call X::X(X&&)                        |   call X::X(int)
  lea rdi, [rsp+14]                     |   lea rsi, [rsp+15]
  mov esi, 1                            |   lea rdi, [rsp+14]
  call X::X(int)                        |   call X::X(X&&)
  lea rsi, [rsp+14]                     |   xor eax, eax
  lea rdi, [rsp+15]                     |   add rsp, 24
  call X::X(X&&)                        |   ret               
  lea rsi, [rsp+15]
  lea rdi, [rsp+12]
  call X::X(X&&)
  lea rdi, [rsp+13]
  mov esi, 1
  call X::X(int)
  lea rsi, [rsp+13]
  lea rdi, [rsp+15]
  call X::X(X&&)
  lea rsi, [rsp+15]
  lea rdi, [rsp+14]
  call X::X(X&&)
  lea rsi, [rsp+14]
  lea rdi, [rsp+15]
  call X::X(X&&)
  xor eax, eax
  add rsp, 24
  ret
like image 30
Oliv Avatar answered Feb 20 '23 18:02

Oliv