Consider:
struct A {
A();
template<class T> A(T&);
};
struct B {
B();
B(B&);
};
struct C {
A a;
B b;
};
const C c1;
C c2{c1};
All compilers agree that the initialization of c2
is invalid before C++20, and all except Clang agree that it is invalid after C++20. Even Clang 20.1.0 thinks that it is invalid (Thanks @MarekR and @Oersted!). But the trunk version of Clang allows this initialization in C++20 and later.
demo: https://godbolt.org/z/EaE6WYeKK
Is Clang's new behavior correct? Why?
Clang trunk is wrong, but it is very subtle.
All references below are relative to N4861 (C++20 DIS). The contents of section [dcl.init] have been moved to [dcl.init.general] in subsequent working drafts.
First, note that the struct C
is an aggregate type with an unusual implicit copy constructor C(C&)
.
C(C&)
, not the usual C(const C&)
, because the member b
does not have a constructor B(const B&)
, not even a deleted one ([class.copy.ctor]/6, [class.copy.ctor]/7).Second, c2
is list-initialized, but eventually it's direct-initialized from c1
.
c2
is list-initialized because the initializer {c1}
is a braced-init-list
([dcl.init]/(17.1)).c2
is direct-initialized from c1
, because it's an aggregate and the initializer list has a single element of type const C
([dcl.init.list]/(3.2)).Third, the direct-initialization performs overload resolution to select the best constructor ([dcl.init]/(17.6.2)).
The closest candidate is the unusual implicit copy constructor C(C&)
, but it is not viable because C&
cannot bind to a const
lvalue ([over.match.viable]/4).
All these lead us to [dcl.init]/(17.6.2.2), C++20's parenthesized aggregate initialization:
Otherwise, if no constructor is viable, the destination type is an aggregate class, and the initializer is a parenthesized
expression-list
, the object is initialized as follows. Let e1, …, en be the elements of the aggregate ([dcl.init.aggr]). Let x1, …, xk be the elements of theexpression-list
. If k is greater than n, the program is ill-formed. The element ei is copy-initialized with xi for 1 ≤ i ≤ k. The remaining elements are initialized with their default member initializers, if any, and otherwise are value-initialized. For each 1≤i<j≤n, every value computation and side effect associated with the initialization of ei is sequenced before those associated with the initialization of ej.
... but it is not applicable, because the initializer c1
is not a parenthesized expression-list
. (Apparently Clang trunk does not realize this fact.)
Hence, [dcl.init]/(17.6.2.3) applies, which says:
- Otherwise, the initialization is ill-formed.
However, if we change C c2{c1};
to C c2(c1);
(that is, change braces to parens), then Clang trunk (and only Clang trunk) will be correct, since in this case [dcl.init]/(17.6.2.2) will indeed apply, which will initialize c2.a
with c1
and value-initialize c2.b
.