Why Does Aggregate Initialization Not Work Anymore Since C++20 If a Constructor Is Explicitly Defaulted or Deleted

Why does aggregate initialization not work anymore since C++20 if a constructor is explicitly defaulted or deleted?

The abstract from P1008, the proposal that led to the change:

C++ currently allows some types with user-declared constructors to be initialized via aggregate initialization, bypassing those constructors. The result is code that is surprising, confusing, and buggy. This paper proposes a fix that makes initialization semantics in C++ safer, more uniform,and easier to teach. We also discuss the breaking changes that this fix introduces.

One of the examples they give is the following.

struct X {
int i{4};
X() = default;
};

int main() {
X x1(3); // ill-formed - no matching c’tor
X x2{3}; // compiles!
}

To me, it's quite clear that the proposed changes are worth the backwards-incompatibility they bear. And indeed, it doesn't seem to be good practice anymore to = default aggregate default constructors.

Brace initialization when move constructor is deleted

It is specific to C++20. Since C++20 a class is no longer aggregate if it there is any user-declared constructor at all, even if it is only defaulted or deleted. Aggregate initialization won't work anymore.

This is a backwards-compatibility breaking change and you can't really get back the old behavior. You will have to adjust your code to the change.

If you want to disable the move constructor, you need to add a proper constructor to use for the initialization, i.e. one taking an int as argument and initializing bar with it, and you can't rely on aggregate initialization anymore. Alternatively you can add a non-movable member with default initializer as last member of the class. Then it won't be required to be provided an initializer in the aggregate-initialization, but will make the whole class non-movable.

It is however a bit surprising that you need to disable the move constructor manually at all. If the class is aggregate without any declared constructors it will be movable if and only if all its contents are movable. That should usually be the expected behavior as an aggregate class just lumps together the individual members without adding any further class invariants.

Why does defining an empty copy ctor beside a deleted default ctor make a value initialization with empty list fail?

Without the user-provided copy constructor, A is an aggregate. Yes, even though we deleted the default constructor. It's something that was addressed in C++20.

So, prior to C++20, A a{}; is aggregate initialization, and so doesn't use the deleted constructor. When you uncomment the copy constructor, A stops being an aggregate, thus turning the aggregate initialization into value initialization. So the initialization of a will attempt to call the deleted constructor.

To divine the meaning of an initializer from the standard, one typically starts at [dcl.init]/16. Going through the bullets, one can find how the properties of the initializer (when matched with the properties of the types in question) will affect the way initialization occurs.

Why does a defaulted default constructor depend on whether it is declared inside or outside of class?

Note that if you actually try and instantiate B then you'll also get the error that B::B() is deleted: https://gcc.godbolt.org/z/jdKzv7zvd

The reason for the difference is probably that when you declare the C constructor, the users of C (assuming the definition is in another translation unit) have no way of knowing that the constructor is in fact deleted. At best this would lead to some confusing linker error, at worst it'd just crash at runtime. In the case of B all users of B will immediately be able to tell that the default constructor is deleted so it does no harm (even if it makes no sense as warned by clang) to declare it as defaulted.

simple code that compile in c++17 produce error with c++20

There was a change in the C++ standard between C++17 and C++20 for aggregate initialization. Have a look at aggregate initialization (cppreference)

Look at the explanation section:

  • since c++11, until c++20: no user-provided, inherited, or explicit constructors (explicitly defaulted or deleted constructors are allowed)
  • since C++20: no user-declared or inherited constructors

You have declared constructors in your class/struct, so as of C++20 you can't have this kind of initialization. You'd have to declare your own constructor with initialization list

[edit: adding some detailed examples]

Or remove all the defaulted constructors.

You could also have a constructor from std::array:

struct Matrix2 {
std::array<double,9> ptr;

// defaults
Matrix2() = default; // constructor
Matrix2(const Matrix2&) = default; // copy constructor
Matrix2(Matrix2&&) = default; // move constructor
Matrix2& operator=(const Matrix2&) = default; // copy assignment operator
Matrix2& operator=(Matrix2&&) = default; // move assignment operator
constexpr Matrix2(const std::array<double, 9> & arr)
: ptr(arr) {}
~Matrix2() = default; // destructor
};

constexpr Matrix2 Id2() {
// not nice, requires extra brace.
// not nice, potentially slower if the compiler misses
// the opportunity to elide the copy of the array.
return {{ 1.0 , 0.0 , 0.0 ,
0.0 , 1.0 , 0.0 ,
0.0 , 0.0 , 1.0 }};
}

int main () {
auto a = Id2();
}

Or with initialization list:

struct Matrix2 {
double ptr[9];

// defaults
Matrix2() = default; // constructor
Matrix2(const Matrix2&) = default; // copy constructor
Matrix2(Matrix2&&) = default; // move constructor
Matrix2& operator=(const Matrix2&) = default; // copy assignment operator
Matrix2& operator=(Matrix2&&) = default; // move assignment operator
constexpr Matrix2(const std::initializer_list<double> & init)
:ptr{}
{
// not nice, this is not as good as the validation
// the compiler does for aggregate initialization.
assert(std::size(init) <= std::size(ptr));
std::copy(std::begin(init), std::end(init), ptr);
}
~Matrix2() = default; // destructor

};

constexpr Matrix2 Id2() {
return { 1.0 , 0.0 , 0.0 ,
0.0 , 1.0 , 0.0 ,
0.0 , 0.0 , 1.0 };
}

int main () {
auto a = Id2();
}

Is a float member guaranteed to be zero initialized with {} syntax?

Because S is an aggregate, S{} will perform aggregate initialization. The rule in the standard about how members are initialized when there are no initializers in the list is basically what you cited:

  • If the element has a default member initializer ([class.mem]), the element is initialized from that initializer.
  • Otherwise, if the element is not a reference, the element is copy-initialized from an empty initializer list ([dcl.init.list]).

So for b, that's the equivalent of float b = {};. Per the rules of list initialization, we have to get all the way down to 3.10:

Otherwise, if the initializer list has no elements, the object is value-initialized.

And value initialization will initialize a float to 0.



Related Topics



Leave a reply



Submit