Here’s a quick rundown of the situation:
Base
is implicitly convertible from anint
.Base
is not an aggregate, since it has a user-provided constructor.Derived
is not convertible from anint
(implicitly or otherwise), since base-class constructors are not inherited unless you explicitly inherit them (which you didn’t).Derived
is not an aggregate due to having a base class… in C++14.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.