索引新的地图元素并分配给它读取的内容是未定义行为还是未指定行为?

4

在回答这个问题后,有一场长时间的讨论,关于问题中的代码是否存在未定义行为。下面是代码:

std::map<string, size_t> word_count;
word_count["a"] = word_count.count("a") == 0 ? 1 : 2;

首先,很明显这至少是未指定的。根据评估的顺序不同,结果也会有所不同。在我的回答中,我详细解释了四种情况,包括哪一边被先评估以及元素是否存在于此之前。

还有一个简短的表格:

(x = 0) = (x == 0) ? 1 : 2; //started as
(x = 0) = (y == "a") ? 1 : 2; //changed to

我认为更像这样:

(x = 0, x) = (x == 0) ? 1 : 2; //comma sequences x, like [] should

最终,我找到了一个似乎适合我的示例:
i = (++i,i++,i); //well-defined per SO:Undefined Behaviour and Sequence Points

回到原始代码,我将其分解为相关的函数调用,以便更容易跟踪:

operator=(word_count.operator[]("a"), word_count.count("a") == 0 ? 1 : 2);
   ^       inserts element^                        ^reads same element
   |
assigns to element

如果word_count["a"]不存在,则认为它将在没有顺序的情况下被分配两次。个人认为如果我认为的两件事情是真的,那么这种情况不会发生:
  1. 在评估选择的一侧之前,必须评估整个一侧。

  2. 例如word_count["a"] = 1的结构具有良好定义的行为,即使在插入元素后再进行赋值的情况下也是如此。

这两个语句是否正确?最终,这是否真的是未定义的行为,如果是,为什么第二个语句有效(假设它有效)?如果第二个语句是错误的,我相信世界上所有的myMap[i]++;都是不合法的。
有用的链接:未定义的行为和序列点

在C语言的上下文中提出了一个相关的问题:http://stackoverflow.com/questions/13935904/is-x-y-y-x-undefined-or-unspecified-and-if-unspecified-what-can-it - Pascal Cuoq
@PascalCuoq,谢谢,它看起来非常相关。问题是它是否适用于C++(几乎可以肯定是),以及是否适用于创建地图中的新元素。 - chris
似乎有很多函数调用在各个地方引入了序列点。另一方面,如果结果仍未指定,表达式的实际用途是什么? - Bo Persson
@BoPersson,我确保在我的答案中没有偏离问题。我提供了一个明确定义的方法来完成它(除非语句2实际上是错误的)。在那场漫长而令人心烦意乱的讨论之后,我只是对它到底在做什么感兴趣。 - chris
2个回答

5

行为未指定,但不是未定义的

请注意,在以下表达式中:

word_count["a"] = word_count.count("a") == 0 ? 1 : 2;
//              ^

使用标记为^的赋值运算符是内置赋值运算符,因为std::mapoperator []返回一个size_t&
根据C++11标准第5.17/1段关于内置赋值运算符的规定:
引用:

赋值运算符(=)和复合赋值运算符都是从右到左结合。[...] 在所有情况下,赋值都是在计算右操作数和左操作数的值之后、赋值表达式的值之前进行的。对于一个不确定顺序的函数调用,复合赋值的操作是单个评估。

这意味着,在内置赋值中,例如:
a = b

首先,操作数将被评估(顺序未指定),然后执行赋值操作,最后执行整个赋值表达式的值计算。

考虑原始表达式:

word_count["a"] = word_count.count("a") == 0 ? 1 : 2;
//              ^

由于上面引用的段落,在任何情况下,同一对象不会有两个未排序的赋值:在地图中不存在键“a”的情况下,用^标记的赋值将始终在执行operator [](作为左侧表达式的评估的一部分)之后被排序。然而,基于哪个赋值先评估,表达式将具有不同的结果。因此,行为是未指定的,但不是未定义的。

谢谢,你不知道经历了那么多之后这让我感到非常宽慰。 - chris
@chris:我又陷入了怀疑之中:1.9/15“如果标量对象上的副作用相对于同一标量对象上的另一个副作用或使用同一标量对象的值计算是无序的,那么行为是未定义的。”在这里,我不确定左侧表达式是否包含修改标量值的副作用,而该值在评估右侧表达式时被读取。 - Andy Prowl
我不知道为什么直到现在我甚至都没有看到那个通知,但是哇,想到未定义行为的方式会让你得动脉瘤的数量是惊人的。我不确定。 - chris
@chris:我非常赞同。 - Andy Prowl

2

这是未指定的,但不是未定义的。

word_count.operator[]("a")word_count.count("a")是函数调用。函数执行被标准保证不会交错 - 要么第一个完全在第二个之前序列化,要么反过来。

具体定义可以根据标准而变化,在C++11中相关条款在1.9/15:

在调用函数的每个评估(包括其他函数调用)中,在执行被调用函数的主体之前或之后没有明确规定地与被调用函数的执行 不确定地排序9

9) 换句话说,函数执行不互相交错。

indeterminately sequenced 在1.9/13中定义:

当A在B之前序列化或B在A之前序列化时,评估A和B是不确定排序的,但未指定哪个。

例如,对以下内容进行评估:

word_count["a"] = word_count.count("a");

由三个部分组成:

  1. word_count.operator[]("a") 的执行
  2. word_count.count("a") 的执行
  3. 赋值操作

< 表示 '被序列化于'。标准引用的部分保证了1 < 2 或者 2 < 1。@Andy Prowl 答案中引用的部分也展示了同时有 1 < 32 < 3。所以,只有两种情况:

  • 1 < 2 < 3
  • 2 < 1 < 3

在这两种情况下,所有内容都被正确地序列化,不存在 UB。


@AndyProwl,哈哈!我以前从没见过这种情况 :) - chris
1
@chris:这种情况很常见! :) - Andy Prowl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接