Why does this code compile without errors in C++17?
(For a thorough walk-through of this topic, see the blog article The fickle aggregate)
Aggregate initialization
Class Ax
is an aggregate in C++11, C++14 and C++17, as it has no user-provided constructors, which means that Ax{}
is aggregate initialization, bypassing any user-declared constructors, even deleted ones.
struct NonConstructible {
NonConstructible() = delete;
NonConstructible(const NonConstructible&) = delete;
NonConstructible(NonConstructible&&) = delete;
};
int main() {
//NonConstructible nc; // error: call to deleted constructor
// Aggregate initialization (and thus accepted) in
// C++11, C++14 and C++17.
// Rejected in C++20 (error: call to deleted constructor).
NonConstructible nc{};
}
The definition of what is an aggregate class has changed through various standard versions (C++11 through C++20), and these rules can have somewhat surprising consequences. As of C++20, particularly due to the implementation of
- P1008R1: Prohibit aggregates with user-declared constructors
most of the frequently surprising aggregate behaviour has been addressed, specifically by no longer allowing aggregates to have user-declared constructors, a stricter requirement for a class to be an aggregate than just prohibiting user-provided constructors.
User-provided or only user-declared explicitly-defaulted constructors
Note that providing an explicitly-defaulted (or deleted) definition out-of-line counts as a user-provided constructor, meaning that in the following example, B
has a user-provided default constructor, whereas A
does not:
struct A {
A() = default; // not user-provided.
int a;
};
struct B {
B(); // user-provided.
int b;
};
// Out of line definition: a user-provided
// explicitly-defaulted constructor.
B::B() = default;
with the result that A
is an aggregate, whereas B
is not. This, in turn, means that initialization of B
by means of an empty direct-list-init will result in its data member b
being left in an uninitialized state. For A
, however, the same initialization syntax will result in (via aggregate initialization of the A
object and subsequent value initalization of its data member a) zero-initialization of its data member a
:
A a{};
// Empty brace direct-list-init:
// -> A has no user-provided constructor
// -> aggregate initialization
// -> data member 'a' is value-initialized
// -> data member 'a' is zero-initialized
B b{};
// Empty brace direct-list-init:
// -> B has a user-provided constructor
// -> value-initialization
// -> default-initialization
// -> the explicitly-defaulted constructor will
// not initialize the data member 'b'
// -> data member 'b' is left in an unititialized state
This may come as a surprise, and with the obvious risk of reading the uninitialized data member b
with the result of undefined behaviour:
A a{};
B b{}; // may appear as a sound and complete initialization of 'b'.
a.a = b.b; // reading uninitialized 'b.b': undefined behaviour.
In C++17, your example is an aggregate. For C++17 aggregates only need to have no user-provided constructors; user-declared (but explicitly deleted or defaulted) constructors are fine.
In this case, then, aggregate initialization is performed when you do Ax{}
, which doesn't call any of the constructors... including the deleted ones, and so this compiles.
In C++20 the rules were changed so that any user-declared constructors prevent the type from being an aggregate, and so the example will fail to compile.
See also https://en.cppreference.com/w/cpp/language/aggregate_initialization