【问题标题】:Is it defined behavior to reference an early member from a later member expression during aggregate initialization?在聚合初始化期间从后面的成员表达式中引用早期成员是否定义了行为?
【发布时间】:2016-01-01 15:38:48
【问题描述】:

考虑以下几点:

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 元素(例如,&amp;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

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

【问题讨论】:

  • 好问题。据我所知,列表初始化仅确保参数的评估顺序是从左到右的,并且很少有情况不会调用 UB,否则会调用 UB,例如:mystruct x {++i, ++i}; 是明确定义的,尽管 mystruct y(++i, ++i); 是不是。仅此而已。由于您的代码没有被规范转换,因此我认为它调用了 UB。
  • 由于省略而看起来像 UB(“[defns.undefined] 当本国际标准省略任何明确的行为定义时,可能会出现未定义的行为......”)标准说([dcl.init.list]/4) brace-init-list 中的各个初始值设定项按顺序进行评估,但没有说明如何实际将每个初始值设定项应用于聚合的相应成员与该订单进行交互。
  • @IgorTandetnik [dcl.init.aggr]/p7 中的示例会提出其他建议。
  • @johannes-schaub-litb 基于标准或关闭为重复的George Stocker lists here。基本上观点、投票和完整答案我认为应该是相反的。
  • 鉴于 Johannes 指出的缺陷报告和标准讨论线程,我认为在缺陷报告得到澄清之前依赖第一个案例是不明智的。我相应地更新了我的答案。抄送@T.C.

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


【解决方案1】:

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

我们还有12.7Construction and destroy [class.cdtor] 部分,其中说:

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

因此,假设第一个示例确实有效,我看不到让您的第二个示例像您的第一个示例一样工作的方法。

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

在花括号初始化列表的初始化列表中,初始化子句,包括任何由 pack 产生的子句 扩展(14.5.3),按照它们出现的顺序进行评估。也就是说,每个值计算和 与给定初始化子句相关的副作用在每个值计算和边之前排序 与在初始化器列表的逗号分隔列表中跟随它的任何初始化器子句相关联的效果。 [...]

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

MSalters 认为 1.9 部分说:

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

结合:

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

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

作为参考,如果标准没有强加要求,则行为在定义未定义行为的 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 正在初始化一个 类类型的成员,构造函数调用显然是 [intro.execution]p10 中定义的完整表达式,所以它 很自然地假设标量类型也是如此。

但是,我认为标准实际上并没有明确说明这一点 任何地方。

因此,尽管正如在几个地方所指出的那样,当前的实现看起来像我们期望的那样,但在正式澄清或实现提供保证之前依赖它似乎是不明智的。

C++20 更新

使用Designated Initialization proposal: P0329,对于第一种情况,此问题的答案会发生变化。它包含以下部分:

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

聚合元素的初始化按元素顺序进行评估。那是, 与给定元素相关的所有值计算和副作用都在之前排序

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

【讨论】:

【解决方案2】:

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

当聚合由初始化列表初始化时,如 8.5.4 中所指定,初始化列表的元素被视为聚合成员的初始化,按递增的下标或成员顺序。 每个成员都从相应的初始化子句复制初始化。

强调我的

还有

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

让我相信类的每个成员都将按照它们在初始化器列表中声明的顺序进行初始化,并且由于 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 尚未初始化,所以我们正在复制一个未初始化的变量,这是未定义的行为。

【讨论】:

  • 虽然您的第一个结论似乎合理,但没有明确的文字说明在评估列表时成员已初始化。虽然像 mem-initializers 一样思考似乎很直观,但我想人们可以提出一个论点,即不指定它会带来一些优化机会。如果它不是显式的,则它是未定义的。
  • @ShafikYaghmour:根据 [3.10] 的定义,初始化成员是初始化子句的副作用。 (在普通英语中,我们称其为 main 效果,但这是标准的)。由于初始化子句的所有副作用都排在下一个初始化子句之前,因此实际初始化也排在前。
  • ".. associated with a given initializer-clause":这里不清楚存储到非类成员是否被认为是任何表达式的一部分(因此,是否它与相应的初始化子句一起排序)。
  • 请参阅open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1343,这基本上就是这个问题。
【解决方案3】:

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

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

稍后:

当聚合被初始化列表初始化时,如 8.5.4 中所指定,初始化列表的元素 被视为聚合成员的初始值设定项,按递增的下标或成员顺序

(强调我的)

我的理解是mystruct foo{45, foo.i};先用45初始化foo.i,再用foo.i初始化foo.j

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

【讨论】:

  • 我不认为引用意味着你所声称的......它是说第 N 个列表项初始化聚合的第 N 个成员,仅此而已。它没有说是否可以在使用初始化程序之前对其进行评估(尽管我认为可能有另一个关于支撑初始化的一般文本确实指定了这一点)
  • @M.M 实际上,Johannes 提供了相关的缺陷和标准讨论链接,我更新了我的答案。
【解决方案4】:

如何使用用户定义的构造函数获得初始行为(如果它是明确定义的行为)?

通过引用传递参数,该参数引用之前被构造对象的初始化参数,如下:

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

【讨论】:

  • 添加构造函数使引用 foo.i UB 自己。 [class.cdtor]/1:“对于具有非平凡构造函数的对象,在构造函数开始执行之前引用对象的任何非静态成员或基类会导致未定义的行为。”
  • @T.C. ctor 初始值设定项列表是否算作“在构造函数开始之前”?
  • 必须先评估参数,然后才能将其传递给 ctor,所以是的:您正在引用尚不存在的东西。
猜你喜欢
  • 2022-11-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-21
  • 1970-01-01
  • 2013-03-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多