C++11 – Is it Legal to Elide a Non-Trivial Copy/Move Constructor in Initialization?

c++c++11language-lawyer

Given this application:

#include <iostream>

struct X {
  X(int _x)                   { x = _x     + 1; }
  X(const X& that)            { x = that.x + 10; }
  X& operator=(const X& that) { x = that.x + 100; return *this; }
  X(X&& that)                 { x = that.x + 1000; }
  X& operator=(X&& that)      { x = that.x + 10000; return *this; }
  int x;
};

int main() {
  X a(1);
  std::cout << "a.x=" << a.x << std::endl;
  X b = 2;
  std::cout << "b.x=" << b.x << std::endl;
  X c = X(3);
  std::cout << "c.x=" << c.x << std::endl;
  X d = a;
  std::cout << "d.x=" << d.x << std::endl;
}

I expected the output to be:

a.x=2
b.x=1003
c.x=1004
d.x=12

Yet what I get is:

a.x=2
b.x=3
c.x=4
d.x=12

Live example

The only way to get my expected output is to compile with -fno-elide-constructors (example)

I thought the compiler may not elide stuff if doing so will affect the observed behavior, yet GCC, clang and MSVC seem to be doing just that.

Am I missing some general rule or is it specific to object initialization with a temporary?

Best Answer

Copy elision is allowed to happen even if it ignores side effects:

[class.copy]/31: When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects. [...]

A good general rule is to not write code which relies on copy/move constructor side effects, as you can easily get bitten by elision. This is particularly true in C++17, where certain cases of copy elision are mandatory.

Related Question