【问题标题】:How to determine what is 'sequenced before' others?如何确定什么是“先于”其他人?
【发布时间】:2012-03-22 16:24:34
【问题描述】:

我浏览了 this excellent answer 关于 C++11 中未定义行为和序列化 [之前/之后] 关系的内容。我了解二元关系的概念,但我不知道管理排序的新规则是什么。

对于这些熟悉的示例,排序规则如何应用?

  1. i = ++i;
  2. a[++i] = i;

更具体地说,新的 C++11 排序规则是什么?

我正在寻找一些规则,例如(这个完全是编造的)

'=' 语句的lhs 始终排在rhs 之前,因此首先计算。

如果这些在标准本身中可用,有人可以在这里引用相同的内容吗?

【问题讨论】:

  • 序列点的规则应该没有变化,因为旧代码必须像以前一样工作。它只是为了应对线程而改写。
  • @BoPersson:但新规则可以(并且现在)更精确。以前是 UB 的东西可以在 C++11 中很好地定义而不会破坏旧代码。 :)

标签: c++ c++11 language-lawyer


【解决方案1】:

sequenced-before 关系及其相关规则是对序列点的先验规则的“整理”,其定义方式与其他内存模型关系如 happens-beforesynchronizes-with 以便精确指定在哪些情况下可以看到哪些操作和效果。

对于简单的单线程代码,规则的结果不变。

让我们从你的例子开始:

1。 i = ++i;

如果i 是诸如int 这样的内置类型,则不涉及函数调用,一切都是内置运算符。因此会发生 4 件事:

(a)++i值计算,即original-value-of-i+1

(b) ++i副作用,它将 original-value-of-i +1 存储回 i

(c) 赋值的值计算,也就是存储的值,这里是++i值计算的结果

(d) 赋值的副作用,它将新值存储到i

所有这些都是sequenced-before下面的完整表达式。 (即它们都由语句的最后一个分号完成)

由于++i等价于i+=1,存储值的副作用sequenced-before值计算 ++i,所以 (b) 是 sequenced-before (a)。

赋值的两个操作数的值计算sequenced-before赋值本身的值计算,依次sequenced-before 存储值的副作用。因此,(a)(c)之前排序,(c)之前排序(d)。

因此我们有 (b) -> (a) -> (c) -> (d),因此在新规则下这是可以的,而在 C++98 下则不行。

如果iclass,则表达式将为i.operator=(i.operator++())i.operator=(operator++(i)),并且operator++ 调用的所有效果都是sequenced-before 致电operator=

2。 a[++i] = i;

如果a 是一个数组类型,而i 是一个int,那么表达式也有几个部分:

(a)i值计算

(b)++i值计算

(c) ++i副作用,它将新值存储回i

(d) a[++i]值计算,它为 value 计算索引的a 的元素返回一个左值 >++i

(e) 赋值的值计算,也就是存储的值,这里是i值计算的结果

(f) 赋值的副作用,将新值存入数组元素a[++i]

再一次,所有这些都是sequenced-before下面的完整表达式。 (即它们都由语句的最后一个分号完成)

同样,由于++i 等价于i+=1,存储值的副作用先排序值计算++i,所以 (c) 是 sequenced-before (b)。

数组索引++i值计算是*sequenced-before`元素选择的值计算,所以(b)是sequenced的-之前 (d)。

赋值的两个操作数的值计算sequenced-before赋值本身的值计算,依次sequenced-before 存储值的副作用。因此,(a) 和 (d) (e) 之前排序,(e) 在 sequenced-before (f) 之前。

因此我们有两个序列: (a) -> (d) -> (e) -> (f) 和 (c) -> (b) -> (d) -> (e) -> (f) )。

不幸的是,(a) 和 (c) 之间没有排序。因此,存储到i副作用对于i 上的值计算未排序的,并且代码显示未定义的行为。 C++11 标准的 1.9p15 再次给出了这一点。

如上,如果i 是类类型,那么一切都很好,因为运算符变成了函数调用,从而强加了顺序。

规则

规则比较简单:

  1. 内置运算符的参数的值计算sequenced-before运算符本身的值计算。 p>

  2. 内置赋值运算符或预增量运算符的副作用sequenced-before结果的值计算。 p>

  3. 任何其他内置运算符的值计算都是该运算符的副作用sequenced-before

  4. 内置逗号运算符左侧的值计算 副作用sequenced-在右侧的值计算 副作用之前。

  5. 一个完整表达式的所有值计算副作用都是sequenced-before下一个完整表达式。

  6. 函数调用的参数的值计算副作用在函数中的第一个完整表达式之前排序。 p>

  7. 值计算副作用函数内部都是sequenced-before结果的值计算

  8. 对于完整表达式中的任意两个函数调用,一个结果的值计算是对另一个的调用sequenced-before,反之亦然.如果没有其他规则指定排序,编译器可以选择。

    因此在a()+b() 中,a()sequenced-before b(),或者 b()sequenced-before a(),但是有没有规则来指定哪个。

  9. 如果有两个副作用修改同一个变量,而另一个sequenced-before,则代码具有未定义的行为。

  10. 如果存在修改变量的副作用,以及读取该变量的值计算,并且两者都不是sequenced-before另一方面,代码具有未定义的行为。

【讨论】:

  • 我认为你把第一个案例弄错了。 ++i 等价于 (i=i+1) 并且赋值计算顺序在副作用“在所有情况下,赋值顺序在左右操作数的值计算之后,赋值表达式的值计算之前”。 (我的 pdf 中的第 5.17 节 [expr.ass])因此两个写操作是有序的,i=++i 是有效的。这也意味着即使在单线程的情况下,新规则也不同于旧规则(i=++i 在旧 C++ 中是 UB)。
  • 你说得对,++i 等同于i+=1。 (5.3.2)。我不确定你的结论是否如下。我得重新检查一下。
【解决方案2】:

在我看来,这是一个比旧的序列点规则复杂得多的规则,而且我不是 100% 肯定我理解正确......无论如何,IIUC 这一切都归结为是否获得你需要的价值已经应用的副作用。

第一种情况

i = ++i;

在这里进行赋值需要正确部分的值,而要获得该值需要已经应用了副作用;因此,这里的赋值是在增量之后排序的,一切都很好。这里重要的一点是,要执行分配,您需要 RHS 的值,并且只需要 LHS 的地址

回顾一下:

  1. 分配在&i++i 之后排序
  2. ++i 在递增之后排序
  3. (传递性)赋值在递增之后排序

i 的值在增量之后只读一次。它写了两次,一次是增量,一次是赋值,但这两个操作是按顺序排列的(首先是增量,然后是赋值)。

第二种情况

a[++i] = i;

在这里,您需要 RHS 的 i 值和 LHS 的 ++i 值。然而,这两个表达式没有排序(赋值运算符没有强加排序),因此结果是未定义的。

回顾一下:

  1. 分配在&a[++i]i 之后排序
  2. &a[++i]++i 之后排序
  3. ++i 在递增之后排序

这里i 的值被读取两次,一次用于赋值的LHS,一次用于RHS。 LHS 部分也进行了修改(增量)。然而,赋值 RHS 的写访问和读访问没有相对于彼此的顺序,因此这个表达式是 UB。

最后的咆哮

让我再说一遍,我不确定我刚才说的是什么……我的强烈观点是,这种新的排序前/后排序方法更难理解。新规则希望只使一些以前定义良好的 UB 表达式(而 UB 是最糟糕的结果),但它也使规则变得更加复杂(它只是“不要在序列点之间更改相同的东西两次“...您不必进行心理拓扑排序来猜测某物是否为 UB)。

从某种意义上说,新规则并没有对 C++ 程序造成损害(UB 是敌人,现在这方面的 UB 越来越少了)但对语言造成了损害> 通过增加复杂性(当然 C++ 不需要的东西是增加了复杂性)。

还请注意,++i 的有趣之处在于返回值是一个左值(这就是为什么++ ++ i 是合法的),所以它基本上是一个地址,逻辑上不需要对返回值进行排序增量后。但标准是这样说的,这是你需要烧入你的神经元的规则。当然,要拥有一个“可用的”++i,您希望该值的用户获得更新后的值,但就++ 操作员而言,仍然可以看到事情(它返回一个不受增量影响的地址)这个顺序正式不需要。

使用新规则,您不仅需要进行心理拓扑排序以查看表达式是否有效,还需要使用您只需要记住的任意序列关系来执行此操作。

虽然您作为程序员当然希望永远不会编写多次更改相同值而没有清晰序列的代码,但您仍然会面临其他程序员编写的代码中的错误......清楚并且您现在需要更努力地思考以了解某些东西是否合法 C++。

【讨论】:

  • @Omnifarious:我已经习惯了 C++ 狂热者,一旦你说 C++ 不是世界上最完美的语言,他们就会开始疯狂尖叫。我喜欢 C++,但我认为复杂性是 C++ 的大问题之一。相反,委员会认为速度是问题(!),所以现在我们有更复杂的规则,例如关于 r 值引用(x&&)和复制构造函数和赋值运算符的默认实现。
  • @6502:我同意 C++ 很复杂,但我不同意只关注速度。移动语义是语义。它们的引入使得容易出错的auto_ptr 被消除并被更好的unique_ptr 取代。同样,threads 的引入允许对现有实践进行标准化,以实现更好的可移植性。速度 C++ 的一个重点,在每个决定上都非常重视,但它不是唯一的重点。至于序列点和序列之前/之后;新规则可能更复杂,但我也倾向于认为它们更直观。时间会证明一切。
  • 对不起,但我还是不明白,特别是这部分 - “在这里做作业你需要正确部分的值,要得到那个值你需要副作用已经应用了,因此这里的赋值是在增量之后排序的..”。为什么= 在第一个示例中强加一个排序,而在第二个示例中没有?谢谢!
  • IMO 忘记新规则是有道理的,而只使用旧的“你不能在同一个完整表达式中读取和修改内存位置”。它使代码更加简洁,您不必担心 UB。
  • @AnthonyWilliams:不。在i=++i 上,我认为您的回答是错误的。 5.17.1(N3126=10-0116) 表示赋值表达式本身的值计算是在赋值之后进行的。 ++i 等价于 (i+=1) (5.3.2),因此它的值计算在递增之后进行排序。因此,i=++i 中的主要赋值在增量赋值之后按传递性排序,并且表达式有效。声称你有 C++ 经验的人可能会弄错这一点,这一事实也证实(尽管是传闻)我对复杂性的咆哮并非完全愚蠢。
猜你喜欢
  • 2012-12-20
  • 1970-01-01
  • 1970-01-01
  • 2017-08-16
  • 2016-02-25
  • 1970-01-01
  • 2016-06-22
  • 2017-10-22
  • 2011-01-10
相关资源
最近更新 更多