为什么这种未定义的行为

Why is this undefined behaviour?

本文关键字:未定义 为什么      更新时间:2023-10-16

下面是示例代码:

X * makeX(int index) { return new X(index); }
struct Tmp {
    mutable int count;
    Tmp() : count(0) {}
    const X ** getX() const { 
        static const X* x[] = { makeX(count++), makeX(count++) };
        return x; 
    }
};

这将报告静态数组构造中 CLang build 500 上的未定义行为。为了简化这篇文章,计数不是静态的,但它不会改变任何东西。我收到的错误如下:

test.cpp:8:44: 警告:对"计数"进行了多次未排序的修改 [-Wunsequenced]

在 C++11 中,这很好;初始化器列表的每个子句都先于下一个子句排序,因此评估是明确定义的。

从历史上看,这些子句可能是未排序的,因此count的两个未排序修改会产生未定义的行为。

(尽管,正如注释中所指出的,即使在那时,它也可能已经明确定义 - 您可能会将标准解释为暗示每个子句都是一个完整表达式,并且每个完整表达式的末尾都有一个顺序点。我将留给历史学家来辩论过时语言的细节。

更新 2

因此,经过一些研究,我意识到这实际上是很好的定义,尽管评估顺序没有指定。把这些碎片放在一起是一个非常有趣的,虽然有一个更普遍的问题涵盖了 C++11 案例,但没有一个涵盖 C++11 之前案例的一般问题,所以我最终创建了一个自我回答问题,初始化项中同一变量的多个突变是否列出了 C++11 之前的未定义行为,涵盖了所有细节。

基本上,看到makeX(count++), makeX(count++)时的本能是将整个事物视为一个完整的表达式,但事实并非如此,因此每个初始者都有一个序列点

更新

正如James指出的那样,它可能不是未定义的pre-C++11,这似乎依赖于将每个元素的初始化解释为一个完整的表达式,但不清楚你绝对可以提出这种说法。

源语言

在 C++11 之前,在一个序列点内多次修改变量是未定义的行为,我们可以看到,通过查看旧标准草案中的相关部分,将是第 5表达式第 4 段说(强调我的):

[...]在上一个和下一个序列点之间,标量对象的存储值应通过表达式的计算最多修改一次。此外,访问先前的值只能用于确定要存储的值。对于完整表达式的子表达式的每个允许顺序,应满足本款的要求;否则,行为是未定义的。

C++11标准草案中,这一变化以及第1.9节计划执行15段的以下措辞说(强调我的):

除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的计算是无序的。[...]如果标量对象的副作用相对于同一标量对象上的另一个副作用或使用同一标量对象的值进行的值计算是未排序的,则行为是未定义的。

我们可以看到,对于第 8.5.4 的初始值设定项列表 列表初始化 第 4 段说:

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

因为在这种情况下,,不是序列点,而是更像数组元素初始化中的分隔符。

换句话说,您在没有序列点的语句中修改同一变量两次(在修改之间)。


编辑:感谢@MikeSeymour:这是C++03以前的问题。似乎在C++11中,评估的顺序是为这种情况定义的