Return from function using copy-list-initialization, no copy/move constructor needed - Where's it stated in C++ 11 standard?

Viewed 113
struct A {
  A() {}
  A(const A&) = delete;
};

A foo() {
  return {}; // Calls A()
}

struct B {
  B(const B&) = delete;
};

B bar() {
  return {}; // Aggregate initialization.
}

foo and bar both compile fine in C++11, because they use copy-list-initialization. There is no copy elision needed.

Where is it mentioned in the C++ standard that in such cases copy/move constructors are not needed?

I can see in [stmt.return] A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization (8.5.4) from the specified initializer list.

I cannot find the section that mentions that in such cases copy/move constructors are not needed.

2 Answers

This is a C++11 (and 14, and 17) issue where aggregate-initialization is allowed to bypass the copy-constructor check.

  • B is an aggregate class (in C++11/14/17)
    • You can verify that in C++17 with the std::is_aggregate type trait
  • You are using list-initialization
    • This performs aggregate initialization, which is treated slightly differently than a regular constructor
  • Per @NicolBolas' nice answer regarding [stmt.return], return {} is like performing copy-list-initialization (aggregate initialzation) of the returned object directly (as opposed to constructing and then returning)

If you had written return B() instead, the compiler would have rejected this code.

The bypass is fixed in C++20 (The compiler will reject your code) per P1008 since B is no longer an aggregate type (C++20 says that a class with any user-declared constructors is not an aggregate).

Where is it mentioned in the C++ standard that in such cases copy/move constructors are not needed?

It isn't. There is no need to mention such a thing because copy-list-initialization is not specified to do any copying or moving.

The behavior of return {...}; is defined as follows:

A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization

So this syntax invokes copy-list-initialization, with the braced-init-list being the initializer and the function's return value object being the object to be initialized. So it is functionally equivalent to T return_value_object = {...};.

[dcl.init.list]/3 explains the entire process of list-initialization, and nowhere does it say that the object to be initialized is copied or moved from some T (where T is the object type being initialized). No temporary object of type T is created or anything of the kind. The braced-init-list simply initializes the object.

So there is no need for a copy or move constructor on T unless the members of the braced-init-list would themselves require one (if you provided an object of type T as a member of the list, for example).

Note that this is not elision. Elision implies that a copy/move would have happened but was optimized out. List initialization doesn't have any copying or moving to optimize away to begin with.

Related