11

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?

rustyx
  • 80,671
  • 25
  • 200
  • 267
  • dupe/related of [this](http://stackoverflow.com/questions/24559875/can-functions-be-optimized-away-if-they-have-side-effects) and/or [this](http://stackoverflow.com/questions/9253316/under-what-conditions-does-c-optimize-out-constructor-calls) – NathanOliver Feb 08 '17 at 13:22
  • Also, `X b = 2;` is initialization, and never uses the assignment operator anyway. – Bo Persson Feb 08 '17 at 13:32

2 Answers2

9

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.

TartanLlama
  • 63,752
  • 13
  • 157
  • 193
  • 3
    To add to this, 12.8.31.3: "*when a temporary class object that has not been bound to a reference (12.2) 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*" – rustyx Feb 08 '17 at 13:41
4

To quote the standard 12.8.3:

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.

(Emphasis mine)

This means that the compiler is allowed to elide the copy even if the copy has side effects. Which is exactly what's happening in your case.

Hatted Rooster
  • 35,759
  • 6
  • 62
  • 122