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

Viewed 1206

In this example, C++17 can construct a Derived class using the Base class's constructor using {}, but not ().
This also works on C++20 for GCC and Clang, but not for MSVC.
Why can I constuct the Derived class this way? Is it safe?

I have looked through the additions and changes in C++17 and can not find what changed to allow this.
It is very possible that I am blind.

#include <iostream>

class Base {
public:
    Base(const int value) {
        std::cout << "Constructed with value: " << value << '\n';
    }
};

class Derived : public Base {
};

int main(){
    // does compile on C++17 with MSVC
    // does not compile pre or post C++17 with MSVC
    // does compile on and post C++17 on GCC and Clang
    // does not compile pre C++17 with GCC and Glang
    Derived foo{ 42 };

    // does not compile on any C++ version with MSVC, GCC or Clang
    Derived bar(42);
}
1 Answers

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.

Related