Search code examples
c++c++17backwards-compatibilityc++20

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


I'm migrating a C++ Visual Studio Project from VS2017 to VS2019.

I'm getting an error now, that didn't occur before, that can be reproduced with these few lines of code:

struct Foo
{
    Foo() = default;
    int bar;
};
auto test = Foo { 0 };

The error is

(6): error C2440: 'initializing': cannot convert from 'initializer list' to 'Foo'

(6): note: No constructor could take the source type, or constructor overload resolution was ambiguous

The project is compiled with /std:c++latest flag. I reproduced it on godbolt. If I switch it to /std:c++17, it compiles fine as before.

I tried to compile the same code with clang with -std=c++2a and got a similar error. Also, defaulting or deleting other constructors generates this error.

Apparently, some new C++20 features were added in VS2019 and I'm assuming the origin of this issue is described in https://en.cppreference.com/w/cpp/language/aggregate_initialization. There it says that an aggregate can be a struct that (among other criteria) has

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

Note that the part in parentheses "explicitly defaulted or deleted constructors are allowed" was dropped and that "user-provided" changed to "user-declared".

So my first question is, am I right assuming that this change in the standard is the reason why my code compiled before but does not anymore?

Of course, it's easy to fix this: Just remove the explicitly defaulted constructors.

However, I have explicitly defaulted and deleted very many constructors in all of my projects because I found it was a good habit to make code much more expressive this way because it simply results in fewer surprises than with implicitly defaulted or deleted constructors. With this change however, this doesn't seem like such a good habit anymore...

So my actual question is: What is the reasoning behind this change from C++17 to C++20? Was this break of backwards compatibility made on purpose? Was there some trade off like "Ok, we're breaking backwards compatibility here, but it's for the greater good."? What is this greater good?


Solution

  • 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.