Why can this derived class be constructed with `{}` and not with `()` on C++17?

Here’s a quick rundown of the situation:

  1. Base is implicitly convertible from an int.
  2. Base is not an aggregate, since it has a user-provided constructor.
  3. Derived is not convertible from an int (implicitly or otherwise), since base-class constructors are not inherited unless you explicitly inherit them (which you didn’t).
  4. Derived is not an aggregate due to having a base class… in C++14.
  5. Derived is an aggregate in C++17, which allows aggregates to have base classes. Derived does not have any constructors provided by the user; again, Base‘s constructor doesn’t matter because it was not inherited.

Given these facts, what’s happening is the following.

Attempting to use {} on a type will first (sort of) check to see if that type is an aggregate; if so, it will perform aggregate initialization using the values in the braced-init-list. Since whether Derived is an aggregate changed between C++14 and C++17, the validity of that initialization changed as well.

Per #4, Derived is not an aggregate in C++14. So list initialization rules will attempt to call a constructor that takes an int. Per #3, Derived has no such constructor. So Derived foo{ 42 }; is il-formed in C++14.

Per #5, Derived is an aggregate in C++17. So list initialization rules will perform aggregate initialization. This is done by copy-initializing each subobject of the aggregate by the corresponding initializer in the braced-init-list. Derived has only one subobject, of type Base, and the braced-init-list only has one initializer: 42. So it will perform copy-initialization of Base by the initializer 42. That will attempt to perform implicit conversion from an int to Base, which is valid per #1.

So Derived foo{ 42 }; is valid in C++17.

Visual Studio may not have implemented C++17’s ruleset correctly.

Leave a Comment