Copy constructor vs std::initializer_list constructor

Viewed 73

Imagine that we have:

struct S {
  struct S {
  S() { printf("%s\n", __PRETTY_FUNCTION__); }
  S(const S&) { printf("%s\n", __PRETTY_FUNCTION__); }
  S(S&&) { printf("%s\n", __PRETTY_FUNCTION__); }
  ~S() { printf("%s\n", __PRETTY_FUNCTION__); }

  S(std::initializer_list<S>) { printf("%s\n", __PRETTY_FUNCTION__); }
};

Which constructors should be called when S s2{S{}};? Is it ok that gcc and clang have different behavior?

Example: https://godbolt.org/z/qQxyp5

gcc (trunk) output:

S::S()
S::S(std::initializer_list<S>)
S::~S()
S::~S()

clang (trunk) output:

S::S()
S::~S()
1 Answers

GCC is correct here; list initialization does not allow for copy elision in C++17.

If you had done S s2(S{});, this would be required to only call S's default constructor, due to [dcl.init]/17.6.1:

If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same class as the class of the destination, the initializer expression is used to initialize the destination object. [ Example: T x = T(T(T())); calls the T default constructor to initialize x.  — end example ]

However, that only applies to copy initialization and direct initialization.

Doing S s2{S{}}; is list-initialization, which is its own entirely separate form of initialization with its own rules. Since S is not an aggregate, [dcl.init.list]3.6 would take over, which says:

Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]).

Calling a constructor means invoking a specific function with a specific set of parameters. And that means that the prvalue S{} must be used to initialize the parameter to the selected constructor. Which means you have to call the copy/move constructor.

Regular, non-guaranteed, elision aren't allowed either. [class.copy.elision]/1 gives the 3 circumstances under which elision is allowed: return localVariableName, throw localVariableName, and catch(TypeName) if the catch matches what was thrown. This case is obviously none of these, so it doesn't qualify for regular elision.

Related