Search code examples
c++c++17language-lawyer

Why are C++ guranteed copy elision not chainable?


As the standard says, C++17 guaranteed copy elision works for returned object or temporary object as argument.

But why not both? as the code shows:

#include <print>

struct S {
  S() { std::println("ctor"); }
  S(const S &other) : data{other.data} { std::println("copy"); }
  int data{42};
};

S make_s() { return {}; }

void add_1(S s) {
  s.data++;
  std::println("copy elided: {}", s.data);
}

S add_1_to(S s) {
  s.data++;
  return s;
}

void takeS(S s) { std::println("s.data: {}", s.data); }

int main() {
  auto s0 = make_s();
  std::println("copy elided: {}", s0.data);
  add_1({});
  auto s2 = add_1_to({});
  std::println("copy not elided: {}", s2.data);
}

the output:

ctor
copy elided: 42
ctor
copy elided: 43
ctor
copy
copy not elided: 43

The 3rd case add_1_to({}) seems just the combination of the first 2 cases, why the copy elision not happen?


Solution

  • Because that would require the compiler to look through the definition of the function when compiling the call to it to see that the parameter itself is going to be returned.

    Traditionally C and C++ are designed so that the compiler doesn't have to see or look into any definition of a function in order to compile a call to it. Typically the definition of a function isn't even available in the same translation unit. All the currently required or permitted copy elisions are designed so that one only needs to know the function declaration at the call site in order to apply them.

    That being said, this could potentially be useful optimization opportunity to allow the compiler e.g. if the function is an inline function. Somebody would have to write a paper with a proposal to the C++ standards committee about it and convince them that giving the compiler this permission would be beneficial. See the similar CWG issue 1049 and CWG issue 6.

    The exact way in which this would be allowed needs to be considered carefully because it would permit the compiler to change the observable behavior of a program. Copy elision is one of the rare cases where the compiler is allowed to change observable behavior, because nothing requires a copy/move constructor to not have other visible side effects that the user might want to be produced (as in your example) and because one can compare addresses of objects even when copy elision is applied, in which case there may be either two different addresses for two distinct objects or the same address because the elision was applied. A user may rely on the addresses not being equal.

    For example the compiler should clearly not be given arbitrary permission to elide all copy/move constructions and alias any objects of the same type. Where exactly the line needs to be drawn isn't so obvious.