Dan*_*aum 42 c++ language-lawyer
从C++(C++ 11)标准,讨论评估顺序的§1.9.15,是以下代码示例:
void g(int i, int* v) {
i = v[i++]; // the behavior is undefined
}
Run Code Online (Sandbox Code Playgroud)
如代码示例中所述,行为未定义.
(注意:具有略微不同的构造的另一个问题的答案i + i++,为什么a = i + i ++未定义且不是未指定的行为,可能适用于此:答案基本上是由于历史原因而未定义的行为,并非出于必要性.但是,该标准似乎暗示了一些未定义的理由 - 请参见下面的引用.此外,该链接问题表明同意该行为应该是未指定的,而在这个问题中,我问的是为什么行为没有明确规定.)
未定义行为标准给出的推理如下:
如果对标量对象的副作用相对于同一标量对象的另一个副作用或使用相同标量对象的值进行的值计算未被排序,则行为未定义.
在这个例子中,我会认为子表达式i++会完全评估之前的子表达式v[...]求值,并且该结果子表达式的评价是i(前增量),但该值的i是增加值的子表达式已经完全后评估.我认为在那一点(在i++完全评估子表达式之后),进行评估v[...],然后进行分配i = ....
因此,尽管增量i是毫无意义的,但我仍然认为这应该被定义.
为什么这种未定义的行为?
Ste*_*sop 42
我认为在表达子表达式v [...]之前,将完全评估子表达式i ++
但为什么你会这么想?
此代码为UB的一个历史原因是允许编译器优化在序列点之间的任何位置移动副作用.序列点越少,优化的机会越多,但程序员越混乱.如果代码说:
a = v[i++];
Run Code Online (Sandbox Code Playgroud)
标准的目的是发出的代码可以是:
a = v[i];
++i;
Run Code Online (Sandbox Code Playgroud)
这可能是两条指令:
tmp = i;
++i;
a = v[tmp];
Run Code Online (Sandbox Code Playgroud)
将超过两个.
在"优化代码"休息的时候a是i,但是标准允许优化反正,说原代码的这种行为时,是不确定a的i.
很容易说标准i++必须在你建议的任务之前进行评估.然后行为将被完全定义,并且将禁止优化.但这并不是C和C++如何开展业务.
还要注意,在这些讨论中提出的许多例子使得更容易分辨出周围的UB而不是一般的UB.这导致人们说"明显"应该定义行为并禁止优化.但考虑一下:
void g(int *i, int* v, int *dst) {
*dst = v[(*i)++];
}
Run Code Online (Sandbox Code Playgroud)
此函数的行为是i != dst在以下情况下定义的,在这种情况下,您希望获得所有优化(这就是C99引入的原因restrict,允许比C89或C++更多的优化).为了给您优化,行为未定义i == dst.当涉及别名时,C和C++标准在程序员不期望的未定义行为与禁止在某些情况下失败的期望优化之间存在细微差别.关于它的问题的数量表明提问者更喜欢一点点优化和一些更明确的行为,但绘制线仍然不是很简单.
除了行为是否完全定义之外,还有它是否应该是UB的问题,或者仅仅是未指定的与子表达式相对应的某些明确定义的操作的执行顺序.C代表UB的原因与序列点的思想有关,并且编译器实际上不需要修改对象的值的概念,直到下一个序列点.因此,不是通过说"值"在某个未指定的点上改变来限制优化器,标准只是说(换言):( 1)任何依赖于下一个序列点之前的修改对象的值的代码, UB; (2)修改修改对象的任何代码都有UB.其中"修改对象"是自从子表达式的一个或多个合法次序中的最后一个序列点以来将被修改的任何对象.
其他语言(例如Java)全面地完成并完全定义表达式副作用的顺序,因此肯定存在针对C的方法的情况.C++只是不接受这种情况.
Yak*_*ont 30
我打算设计一台病态计算机1.它是一个多核,高延迟,单线程系统,具有线程内连接,可以使用字节级指令进行操作.因此,您要求发生某些事情,然后计算机运行(在其自己的"线程"或"任务"中)一组字节级指令,并在操作完成后运行一定数量的周期.
同时,执行的主要线程仍在继续:
void foo(int v[], int i){
i = v[i++];
}
Run Code Online (Sandbox Code Playgroud)
变成伪代码:
input variable i // = 0x00000000
input variable v // = &[0xBAADF00D, 0xABABABABAB, 0x10101010]
task get_i_value: GET_VAR_VALUE<int>(i)
reg indx = WAIT(get_i_value)
task write_i++_back: WRITE(i, INC(indx))
task get_v_value: GET_VAR_VALUE<int*>(v)
reg arr = WAIT(get_v_value)
task get_v[i]_value = CALC(arr + sizeof(int)*indx)
reg pval = WAIT(get_v[i]_value)
task read_v[i]_value = LOAD_VALUE<int>(pval)
reg got_value = WAIT(read_v[i]_value)
task write_i_value_again = WRITE(i, got_value)
(discard, discard) = WAIT(write_i++_back, write_i_value_again)
Run Code Online (Sandbox Code Playgroud)
所以你会注意到我没有等到write_i++_back最后,就在我等待的时候write_i_value_again(我加载了哪个值v[]).而且,事实上,这些写入是唯一写回内存的.
试想一下,如果写内存此电脑设计的很慢的一部分,他们得到了分批进入的是那些获得由确实在每字节为基础的东西并行修改内存单元中处理事情的队列.
所以write(i, 0x00000001)并write(i, 0xBAADF00D)执行无序并行.每个都变成字节级写入,并且它们是随机排序的.
我们写出来0x00,然后0xBA高字节,然后0xAD和0x00下一个字节,然后0xF0 0x00到下一个字节,最后0x0D 0x01以低字节.i中的结果值是0xBA000001少数人所期望的,但对于未定义的操作来说将是有效的结果.
现在,我在那里所做的一切都导致了一个未指定的值.我们还没有崩溃系统.但是编译器可以自由地使它完全未定义 - 可能会向同一批指令中的同一地址发送两个这样的请求到内存控制器实际上会导致系统崩溃.这仍然是编译C++的"有效"方式,也是"有效"的执行环境.
请记住,这是一种将指针大小限制为8位的语言仍然是一个有效的执行环境.C++允许编译而不是winkey目标.
1:正如@ SteveJessop在下面的评论中所指出的,笑话是这个病态计算机的行为很像现代台式计算机,直到你进入字节级操作.intCPU的非原子写入在某些硬件上并不罕见(例如,当intCPU没有按照CPU希望它对齐的方式对齐时).
Pet*_*ker 24
原因不仅仅是历史性的.例:
int f(int& i0, int& i1) {
return i0 + i1++;
}
Run Code Online (Sandbox Code Playgroud)
现在,这个电话会发生什么:
int i = 3;
int j = f(i, i);
Run Code Online (Sandbox Code Playgroud)
当然可以对代码提出要求,f以便很好地定义这个调用的结果(Java这样做),但是C和C++没有强加约束; 这为优化者提供了更多的自由.
您特别参考C++ 11标准,所以我将回答C++ 11的答案.然而,它与C++ 03的答案非常相似,但是排序的定义是不同的.
C++ 11定义了在单个线程上的评估之间的关系之前的顺序.它是不对称的,传递的和成对的.如果某些A评价是不是有些B评价之前测序和B也不是一个前序,那么这两个评价是未测序.
评估表达式包括值计算(计算某些表达式的值)和副作用.副作用的一个实例是对象的修改,这是回答问题的最重要的对象.其他事情也算作副作用.如果副作用相对于同一对象的另一个副作用或值计算未被排序,则您的程序具有未定义的行为.
这就是设置.第一个重要规则是:
在与要评估的下一个全表达式相关联的每个值计算和副作用之前,对与全表达式相关联的每个值计算和副作用进行排序.
因此,在下一个完整表达式之前完全评估任何完整表达式.在你的问题中,我们只处理一个完整的表达式,即i = v[i++]我们不需要担心这一点.下一个重要规则是:
除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的评估是不确定的.
这意味着,a + b例如,评估a和b未评估(可以按任何顺序评估).现在我们最后的重要规则:
在运算符的结果的值计算之前,对运算符的操作数的值计算进行排序.
因此a + b,关系之前的顺序可以用树表示,其中有向箭头表示关系之前的顺序:
a + b (value computation)
^ ^
| |
a b (value computation)
Run Code Online (Sandbox Code Playgroud)
如果两个评估在树的不同分支中发生,则它们是未排序的,因此该树显示相对于彼此的评估a和b未经测序.
现在,让我们对你的i = v[i++]例子做同样的事情.我们利用v[i++]被定义为等同于的事实*(v + (i++)).我们还使用了一些关于后缀增量顺序的额外知识:
++在修改操作数对象之前,对表达式的值计算进行排序.
所以这里我们去(树的节点是一个值计算,除非指定为副作用):
i = v[i++]
^ ^
| |
i? v[i++] = *(v + (i++))
^
|
v + (i++)
^ ^
| |
v ++ (side effect on i)?
^
|
i
Run Code Online (Sandbox Code Playgroud)
在这里,你可以看到,副作用i,i++是在一个单独的分支的使用i在赋值运算符(我打上每一个★这些评估的)的前面.所以我们肯定有未定义的行为!如果您想知道您的评估顺序是否会给您带来麻烦,我强烈建议您绘制这些图表.
所以现在我们得到的问题是,i赋值运算符之前的值无关紧要,因为无论如何我们都要写它.但实际上,在一般情况下,这不是真的.我们可以覆盖赋值运算符,并在赋值之前使用对象的值.标准并不关心我们不使用该值 - 规则被定义为使得任何没有序列效应的值计算将是未定义的行为.没有但是.这种未定义的行为允许编译器发出更优化的代码.如果我们为赋值运算符添加排序,则不能使用此优化.