为什么这个派生类可以在 C++17 上用 `{}` 而不是用 `()` 构造?

Lil*_*ily 37 c++ c++17

在这个例子中,C++17 可以使用 Base 类的构造函数构造派生类{},但不能使用()
这也适用于 GCC 和 Clang 的 C++20,但不适用于 MSVC。
为什么我可以这样构造派生类?安全吗?

我查看了 C++17 中的添加和更改,但找不到允许这样做的更改。
我很可能是瞎子。

#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);
}
Run Code Online (Sandbox Code Playgroud)

Nic*_*las 35

这是情况的快速概述:

  1. Base可以从 隐式转换int
  2. Base 不是聚合,因为它有一个用户提供的构造函数。
  3. Derived不是从一个转换int(隐式或其他方式),因为基类的构造函数是不能继承的,除非你明确地继承他们(你没有)。
  4. Derived不是由于具有基类...的集合体在C ++ 14
  5. Derived C++17 中的聚合,它允许聚合具有基类。Derived没有用户提供的任何构造函数;同样,Base的构造函数无关紧要,因为它没有被继承。

鉴于这些事实,正在发生的事情如下。

尝试{}在类型上使用将首先(某种程度上)检查该类型是否是聚合;如果是这样,它将使用括号初始化列表中的值执行聚合初始化。由于DerivedC++14 和 C++17 之间的聚合是否发生了变化,因此初始化的有效性也发生了变化。

Per #4,Derived不是 C++14 中的聚合。所以列表初始化规则将尝试调用一个带有int. Per #3,Derived没有这样的构造函数。所以Derived foo{ 42 };是IL-形成在C ++ 14。

Per #5,Derived是 C++17 中的聚合。所以列表初始化规则将执行聚合初始化。这是通过由支撑初始化列表中的相应初始化程序复制初始化聚合的每个子对象来完成的。Derived只有一个类型为 的子对象,Base并且花括号初始化器列表只有一个初始值设定项:42。因此它将Base通过初始化程序执行复制初始化42。这将尝试执行从隐式转换intBase,这是每#1有效的。

所以Derived foo{ 42 };在 C++17 中有效。

Visual Studio 可能没有正确实现 C++17 的规则集。

  • C++ 是一种富有表现力且友好的语言。 (25认同)
  • 问题在于,它需要一个 18 行的 Stack Overflow 答案来解释一个应该从整数实例化一个简单类的代码语句,并且不同版本的语言的解释以完全深奥的方式存在很大差异。这太荒谬了。 (6认同)
  • @AsteroidsWithWings:我不明白这不应该涉及复杂性的想法。它涉及使用基类的构造函数来初始化派生类,以及专门用于初始化类的各个子对象的特殊形式的初始化。这将涉及复杂性,因为它“很复杂”。这就是场景的本质。大多数其他语言甚至不允许您这样做,要求您只能通过其显式提供的构造函数来初始化“Derived”。C++ 更自由,但如果你依赖它,这种自由就需要复杂性 (3认同)
  • @AsteroidsWithWings:“*从整数实例化一个简单的类*” 第一次检查时,该类看起来*不能*从整数实例化。这就是复杂性的来源。 (2认同)