为什么成员初始值设定项不能使用括号?

Why can't member initializers use parentheses?

本文关键字:不能 成员 为什么      更新时间:2023-10-16

例如,我不能这样写:

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

我只能这样写:

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

为什么这两种声明语法有区别?

这个选择背后的理由在非静态数据成员初始化式:

的相关建议中明确提到。

Kona中关于标识符作用域的问题:

在2007年9月Kona会议的核心工作组讨论中,出现了一个关于初始化式中标识符范围的问题。我们是否希望允许类作用域具有正向查找的可能性;还是要求初始化式在解析时定义良好?

期望:

类作用域查找的动机是,我们希望能够在不显著改变语义(模直接初始化vs复制初始化)的情况下,在非静态数据成员的初始化项中放入任何可以放入memm初始化项的内容:

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:

不幸的是,这使得" (expression-list) "形式的初始化式在解析声明时具有歧义性:

   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-子句"answers"{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;
};

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

建议:

CWG在Kona进行了6比3的民意调查,支持类作用域查找;这就是本文所建议的,非静态数据成员的初始化器仅限于"= initializer-子句"answers"{initializer-list}"形式。

我们相信

:

问题1:这个问题不会发生,因为我们没有提出()符号。=和{}初始化项符号没有这个问题。

问题2:添加static关键字会产生许多不同,这是其中最小的。

问题3:这不是一个新问题,但与构造函数初始化项中已经存在的初始化顺序问题相同。

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

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

现在,您有一个要初始化的bar类型的数据成员,因此将其定义为

struct A
{
  bar B(foo());
};

但是你上面所做的是声明一个名为B的函数,它返回一个bar对象的值,并接受一个参数,这个参数是一个签名为foo()的函数(返回一个foo,不接受任何参数)。

从StackOverflow上关于这个问题的提问数量和频率来看,这是大多数c++程序员感到惊讶和不直观的事情。添加新的大括号或等号初始化器语法是避免这种歧义并从头开始的一个机会,这可能是c++委员会选择这样做的原因。

bar B{foo{}};
bar B = foo();

如上两行声明了一个名为Bbar类型的对象。


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

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

第一行将v1初始化为包含两个元素121的vector。第二个函数创建一个包含12元素的向量v2,每个元素初始化为1

注意这个规则——如果一个类型定义了一个接受initializer_list<T>的构造函数,那么当该类型的初始化项是带括号的init-list时,总是首先考虑该构造函数。只有当使用initializer_list的构造函数不可行时,才会考虑其他构造函数。

相关文章: