【问题标题】:Are multiple mutations within initializer lists undefined behavior?初始化程序中的多个突变是否列出了未定义的行为?
【发布时间】:2023-03-03 09:07:20
【问题描述】:

我对初始化列表和序列点很好奇。不久前我读到初始化列表中的评估顺序是从左到右的。如果是这样,那么评估点之间肯定存在某种序列点,我错了吗?所以说是以下有效代码吗?有什么会导致未定义的行为吗?

int i = 0;

struct S {
    S(...) {} 
    operator int() { return i; }
};

int main() {
    i = S{++i, ++i};
}

感谢所有回复。

【问题讨论】:

    标签: c++ c++11 undefined-behavior initializer-list


    【解决方案1】:

    是的,代码是有效的并且没有未定义的行为。初始化器列表中的表达式在计算S 的构造函数之前从左到右进行计算和排序。因此,您的程序应始终将值 2 分配给变量 i

    引用 C++ 标准的第 8.5.4 节:

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

    因此,发生的事情是:

    1. ++i 被评估,产生 i = 1S 的构造函数的第一个参数);
    2. ++i 被评估,产生 i = 2S 的构造函数的第二个参数);
    3. S的构造函数被执行;
    4. 执行S的转换运算符,返回值2
    5. 2 分配给i(已经有值2)。

    该标准的另一个相关段落是第 1.9/15 节,其中还提到了确实具有未定义行为的类似示例:

    i = v[i++]; // the behavior is undefined
    i = i++ + 1; // the behavior is undefined
    

    但是,同一段说:

    "除非另有说明,单个运算符的操作数和单个表达式的子表达式的计算是无序的。[...]调用函数时(无论函数是否内联),与任何参数表达式或与指定被调用函数的后缀表达式相关的每个值计算和副作用,都在被调用函数主体中的每个表达式或语句执行之前排序。 em>"

    因为 1) 初始化列表中的表达式的求值顺序是从左到右的,2) S 的构造函数的执行顺序是在初始化列表中所有表达式的求值之后,以及 3)对i 的赋值是在S(及其转换运算符)的构造函数执行之后排序的,行为是明确定义的。

    【讨论】:

    • 你确定吗?在 C++03 中,i = ++i;(例如)是未定义的行为,即使评估顺序得到保证,因为i 在序列点之间被修改了两次。我不是 C++11 专家——而且我知道 C+11 抛弃了序列点的概念——但这些例子在我看来非常相似。
    • @Nemo:C++11 仍然有一个排序的概念,它只是比“序列点”的概念复杂一点,以便考虑多个线程。但是,答案缺少引用的那句话后面的更关键的句子:“也就是说,与给定初始化子句相关的每个值计算和副作用都是之前排序的每个值计算和副作用与任何初始化器子句相关联,在初始化器列表的逗号分隔列表中。"所以代码确实定义明确。
    • @Nemo 但是我的int() 重载中没有序列点吗?定义行为?
    • @MikeSeymour:你确定这句话的一部分与此相关吗?它只是解释了表达式求值是在初始化列表中排序的,但似乎没有提到 Nemo 对operator = 左侧和右侧的怀疑。我相信 1.9/15 可以解决这个问题。我错了吗?
    • @Nemo this 在 C++11 之前也有很好的定义,尽管评估顺序未指定。我创建了一个 self answered question 来覆盖这个 C++11 之前的版本,因为这是另一个问题的争论来源,我自己解决这个问题很有启发性。
    【解决方案2】:

    是的,您确实有未定义行为的情况。

    以下是导致未定义行为的示例:

    • 一个变量在一个序列点内多次改变。作为一个 典型的例子,i=i++ 表达式经常被引用在哪里 第 i 个变量的赋值及其增量在 同时。要了解有关此类错误的更多信息,请阅读该部分 "sequence points"
    • 在初始化之前使用变量。发生未定义的行为 尝试使用变量时。
    • 使用新的 [] 运算符和后续版本分配内存 使用删除运算符。例如:T *p = new T[10];删除 p;. 正确的代码是:T *p = new T[10];删除 [] p;。

    已编辑

    还有你的代码 S{++i, ++i};不是为 VS2012 编译的。可能你的意思是 S(++i, ++i);? 如果您使用“()”,则存在未定义的行为。在其他情况下,您的源代码不正确。

    【讨论】:

    • 这里没有未定义的行为:大括号初始化语法是常规的 C++11,而 VS2012 不支持它的事实是 that IDE 的限制。使用大括号初始化时,根据我在回答中引用的标准段落,表达式从左到右进行评估。其次,他在初始化变量之前使用它。该变量是全局变量并初始化为 0。因此,如果是您否决了我的答案,您可能需要重新考虑您的决定 :-) 这个答案对于 C++11 是不正确的。
    • C++11 直到 VS2015 才真正得到完全支持。 VS2013 支持大括号初始化,但 VS2012 不支持。
    猜你喜欢
    • 2013-11-21
    • 1970-01-01
    • 2016-01-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-18
    相关资源
    最近更新 更多