是否定义了在聚合初始化期间从后面的成员表达式引用早期成员的行为?

时间:2015-10-05 03:26:50

标签: c++ c++11 language-lawyer c++14 undefined-behavior

请考虑以下事项:

struct mystruct
{
    int i;
    int j;
};

int main(int argc, char* argv[])
{
    mystruct foo{45, foo.i};   

    std::cout << foo.i << ", " << foo.j << std::endl;

    return 0;
}

请注意在聚合初始值设定项列表中使用foo.i

g++ 5.2.0输出

  <45> 45

这是明确定义的行为吗?此聚合初始值设定项中的foo.i始终保证引用正在创建的结构的i元素(例如,&foo.i将引用该内存地址)?

如果我向mystruct添加显式构造函数:

mystruct(int i, int j) : i(i), j(j) { }

然后我收到以下警告:

main.cpp:15:20: warning: 'foo.a::i' is used uninitialized in this function [-Wuninitialized]
     a foo{45, foo.i};
                ^
main.cpp:19:34: warning: 'foo.a::i' is used uninitialized in this function [-Wuninitialized]
     cout << foo.i << ", " << foo.j << endl;

代码编译,输出为:

  

45,0

显然,这会有所不同,我假设这是未定义的行为。是吗?如果是这样,为什么这个和没有构造函数之间的区别?而且,如何使用用户定义的构造函数获取初始行为(如果它是明确定义的行为)?

4 个答案:

答案 0 :(得分:14)

你的第二种情况是未定义的行为,你不再使用聚合初始化,它仍然是列表初始化,但在这种情况下你有一个被调用的用户定义的构造函数。为了将第二个参数传递给构造函数,它需要计算foo.i,但它尚未初始化,因为您还没有输入构造函数,因此您生成了一个不确定的值producing an indeterminate value is undefined behavior

我们还有章节12.7构建和销毁[class.cdtor],其中说:

  

对于具有非平凡构造函数的对象,引用该对象的任何非静态成员或基类   在构造函数开始执行之前导致未定义的行为[...]

所以我没有看到让你的第二个例子像第一个例子一样工作的方法,假设第一个例子确实有效。

你的第一个案例似乎应该很好地定义,但我在草案标准中找不到一个似乎明确的参考。也许它是缺陷,但否则它将是未定义的行为,因为标准没有定义行为。该标准告诉我们的是,初始化器按顺序进行评估,副作用按顺序进行排序,来自8.5.4 [dcl.init.list] 部分:

  

在braced-init-list的initializer-list中,initializer-clause,包括pack中的任何结果   扩展(14.5.3),按它们出现的顺序进行评估。也就是说,每个值计算和   在每个值计算和侧面之前,对与给定初始化子句相关联的副作用进行排序   与在初始化列表的逗号分隔列表中跟随它的任何initializer子句相关联的效果。 [...]

但我们没有明确的文字说明在评估每个元素后会对成员进行初始化。

MSalters争辩说1.9部分说:

  

访问由volatile glvalue(3.10)指定的对象,修改对象,调用库I / O   函数,或调用执行任何这些操作的函数都是副作用,这些都是   执行环境的状态。 [...]

结合:

  

[...]非常值计算和与给定初始化子句相关的副作用在每个值计算和与其后的任何初始化子句相关联的副作用之前被排序[...]

足以保证在评估初始化程序列表的元素时初始化聚合的每个成员。虽然从the order of evaluation of the initializer list was unspecified开始,这在C ++ 11之前不适用。

作为参考,如果标准没有强制要求,则行为未定义来自定义未定义行为的部分1.3.24

  

本国际标准没有要求的行为   [注意:当本国际标准忽略任何明确的定义时,可能会出现未定义的行为   行为或[...]

更新

Johannes Schaub指出defect report 1343: Sequencing of non-class initialization和标准讨论主题Is aggregate member copy-initialization associated with the corresponding initializer-clause?Is copy-initialization of an aggregate member associated with the corresponding initializer-clause?都是相关的。

他们基本上指出第一个案例目前尚未指定,我将quote Richard Smith

  

所以唯一的问题是,初始化s.i的副作用是什么   “与”完整表达“5”的评价相关联?我认为   唯一合理的假设是:如果5正在初始化a   类类型的成员,构造函数调用显然是其中的一部分   [intro.execution] p10中定义的完整表达式,所以它   很自然地假设标量类型也是如此。

     

但是,我不认为该标准实际上明确地说明了这一点   任何地方。

因此,虽然如某些地方所示,当前的实施看起来像我们期望的那样,但在正式澄清或实施提供保证之前依赖它似乎是不明智的。

C ++ 20更新

Designated Initialization proposal: P0329对于第一个案例,此问题的答案会更改。它包含以下部分:

  

在11.6.1 [dcl.init.aggr]中添加一个新段落:

     

聚合元素的初始化按元素顺序进行评估。那是,   在

之前,对与给定元素相关的所有值计算和副作用进行排序

我们可以看到这反映在latest draft standard

答案 1 :(得分:12)

来自[dcl.init.aggr] 8.5.1(2)

  

当初始化程序列表初始化聚合时,如8.5.4中所述,初始化程序列表的元素将作为聚合成员的初始化程序,增加下标或成员顺序。 每个成员都是从相应的初始化子句复制初始化的。

强调我的

并且

  

在braced-init-list的initializer-list中,initializer-clause(包括pack扩展(14.5.3)产生的任何结果)按照它们出现的顺序进行评估。也就是说,与给定的initializer子句相关联的每个值计算和副作用在每个值计算和副作用之前都会在与初始化列表的逗号分隔列表中的任何initializer子句相关联之前进行排序。

让我相信该类的每个成员将按照在初始化列表中声明的顺序进行初始化,并且在我们评估初始化foo.i之前初始化j这应该是定义的行为。

这也是[intro.execution] 1.9(12)

备份的
  

访问由volatile glvalue(3.10)指定的对象,修改对象,调用库I / O函数,或调用执行任何这些操作的函数都是一面效果,它们是执行环境状态的变化。

强调我的

在你的第二个例子中,我们没有使用聚合初始化而是列表初始化。 [dcl.init.list] 8.5.4(3)有

  

对象或类型T的引用的列表初始化定义如下:
  [...]
   - 否则,如果T是类类型,则考虑构造函数。列举了适用的构造函数   通过重载决策选择最好的一个(13.3,13.3.1.7)。

所以现在我们会调用你的构造函数。当调用构造函数foo.i尚未初始化时,我们正在复制未初始化的未定义行为变量。

答案 2 :(得分:1)

我的第一个想法是UB,但你完全处于聚合初始化的情况。用于C ++ 11规范的n4296草案在8.5.1 Aggregates [dcl.init.aggr]段落中是明确的:

  

聚合是一个数组或类,没有用户提供的构造函数,没有私有或受保护的非静态数据成员,没有基类,也没有虚函数

随后:

  

当初始化程序列表初始化聚合时,如8.5.4中所述,初始化程序列表的元素   被视为聚合成员的初始化者,增加下标或成员订单

(强调我的)

我的理解是mystruct foo{45, foo.i};首先将foo.i初始化为45,然后将foo.j初始化为foo.i

我无论如何都不敢在实际代码中使用它,因为即使我相信它是由标准定义的,我也会担心编译器程序员有不同的想法......

答案 3 :(得分:-2)

  

如何使用用户定义的构造函数获取初始行为(如果它是定义良好的行为)?

通过引用传递参数,该参数引用先前已构建对象的初始化参数,如下所示:

 mystruct(int i, int& j):i(i),j(j)