为什么成员初始化不能使用括号?

41
例如,我不能写这个:

For example, I cannot write this:

class A
{
    vector<int> v(12, 1);
};

我只能写这个:

class A
{
    vector<int> v1{ 12, 1 };
    vector<int> v2 = vector<int>(12, 1);
};

为什么这两种声明语法会有差异?


前者调用一个vector<int>构造函数,其输入为12和1。后者调用一个vector<int>构造函数,其输入为初始化列表。它们在本质上是不同的。 - druckermanly
1
标准引用的原因是因为语法是 声明符号 大括号或等于初始化器(可选) - chris
2个回答

38
这个选择背后的理由在相关的提案中明确说明了,它与非静态数据成员初始化器有关:

Kona 中提出的一个问题是标识符的作用域:

在 2007 年 9 月在 Kona 召开的核心工作组讨论中,出现了一个关于初始化器中标识符作用域的问题。我们是否希望允许类作用域,以便进行前向查找;或者我们要求初始化器在解析时定义良好?

所需的:

使用类作用域查找的动机在于,我们希望能够将任何放入成员初始化程序的内容也放入非静态数据成员的初始化器中,而不会显著改变语义(除了直接初始化和复制初始化的区别):

int x();

struct S {
    int i;
    S() : i(x()) {} // currently well-formed, uses S::x()
    // ...
    static int x();
};

struct T {
    int i = x(); // should use T::x(), ::x() would be a surprise
    // ...
    static int x();
};

问题 1:

不幸的是,这使得“(表达式列表)”形式的初始化在解析声明时变得模糊不清:

   struct S {
        int i(x); // data member with initializer
        // ...
        static int x;
    };

    struct T {
        int i(x); // member function declaration
        // ...
        typedef int x;
    };

一个可能的解决方案是依赖于现有规则,即如果声明可以是对象或函数,则它是一个函数:
 struct S {
        int i(j); // ill-formed...parsed as a member function,
                  // type j looked up but not found
        // ...
        static int j;
    };

类似的解决方案是应用另一个现有规则,目前仅在模板中使用。如果T可以是类型或其他东西,则它是其他东西;如果我们确实指的是类型,则可以使用“typename”:
struct S {
        int i(x); // unabmiguously a data member
        int j(typename y); // unabmiguously a member function
    };

这两种解决方案都引入了许多用户可能会误解的细节(正如在 comp.lang.c++ 上关于为什么块作用域中的“int i();”不声明默认初始化 int 的问题有很多提问所证明的那样)。

本文提出的解决方案是只允许“= initializer-clause”和“{initializer-list}”形式的初始化器。这在大多数情况下解决了歧义问题,例如:

HashingFunction hash_algorithm{"MD5"};

在这里,我们不能使用=表单,因为HasningFunction的构造函数是显式的。在特别棘手的情况下,可能需要两次提到类型。考虑:
   vector<int> x = 3; // error:  the constructor taking an int is explicit
   vector<int> x(3);  // three elements default-initialized
   vector<int> x{3};  // one element with the value 3

在这种情况下,我们必须使用适当的符号来选择两个选项之一:
vector<int> x = vector<int>(3); // rather than vector<int> x(3);
vector<int> x{3}; // one element with the value 3

问题2:
另一个问题是,因为我们没有提议更改初始化静态数据成员的规则,添加static关键字可能会导致一个良好格式的初始化程序变得不正确:
   struct S {
               const int i = f(); // well-formed with forward lookup
        static const int j = f(); // always ill-formed for statics
        // ...
        constexpr static int f() { return 0; }
    };

问题3:

第三个问题是类范围查找可能会将编译时错误转换为运行时错误:

struct S {
    int i = j; // ill-formed without forward lookup, undefined behavior with
    int j = 3;
};

(除非被编译器捕获,否则我可能会用j的未定义值进行初始化。)

提案:

CWG在Kona进行了6比3的稻草投票,赞成类作用域查找;这就是本文所提出的,对于非静态数据成员的初始值设定项仅限于“=初始化程序”和“{初始化列表}”形式。

我们认为:

问题1:我们不提议使用()符号,因此不会出现这个问题。=和{}初始化符号不会受到这个问题的困扰。

问题2:添加static关键字会带来许多差异,这只是其中最小的一个。

问题3:这不是一个新问题,而是已经存在于构造函数初始化程序中的初始化顺序问题。


4
+1 是为了感谢你挖掘出来并将其格式化,以便于在 Stack Overflow 上使用。 - Cheers and hth. - Alf

25

可能的一个原因是允许括号会很快导致我们回到最令人烦恼的解析。考虑下面的两种类型:

struct foo {};
struct bar
{
  bar(foo const&) {}
};

现在,您有一个类型为bar的数据成员需要初始化,因此您将其定义为
struct A
{
  bar B(foo());
};

你上面所做的是声明一个名为B的函数,它通过值返回一个bar对象,并接受一个单参数,该参数是一个具有签名foo()(返回一个foo并且不带任何参数)的函数。
根据在StackOverflow上提出这个问题的数量和频率来判断,这是大多数C ++程序员都觉得令人惊讶和不直观的事情。添加新的brace-or-equal-initializer语法是避免这种歧义并从头开始的机会,这很可能是C ++委员会选择这样做的原因。
bar B{foo{}};
bar B = foo();

上述两行声明了一个名为Bbar类型对象,正如预期。


除了上面的猜测之外,我想指出你在上面的例子中做了两件截然不同的事情。

vector<int> v1{ 12, 1 };
vector<int> v2 = vector<int>(12, 1);

The first line initializes v1 to a vector that contains two elements, 12 and 1. The second creates a vector v2 that contains 12 elements, each initialized to 1.
请注意这个规则 - 如果一个类型定义了一个接受 initializer_list<T> 的构造函数,那么当类型的初始化器是一个 花括号初始化列表 时,该构造函数总是被首先考虑。只有在不可行时才会考虑其他构造函数。

1
当在参数声明中使用时,foo()是一个函数指针而不是一个函数本身,就像内置数组声明一样。 - Lingxi
@Lingxi 那不就是我说的吗? - Praetorian
1
这有什么比其他地方出现的最棘手的解析更糟糕的呢? - Ben Voigt
@Ben,这并不更糟,你仍然可以写尽可能多的令人烦恼的解析。但是旧语法的含义不能改变,所以唯一的选择是提出具有明确含义的新语法。当然,这一切都是我个人对此的看法。也许如果有人挖掘C++关于此功能的提案,他们可能会找到理由。 - Praetorian
@Praetorian:谢谢,虽然我已经知道了(我在a comment中回答了那个问题,有点匆忙但足够了)。 - Cheers and hth. - Alf
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接