该代码由于未指定子表达式的评估顺序而表现出未指定的行为,尽管它不会调用未定义的行为,因为所有副作用都是在函数内完成的,在这种情况下,这些函数会引入副作用之间的顺序关系。
提案N4228:完善惯用C ++的表达式评估顺序中提到了此示例,其中对问题中的代码说了以下内容:
[...]此代码已由世界各地的C ++专家审查并发布(《 C ++编程语言》,第 4 版。)然而,仅在最近一种工具中才发现其对未指定评估顺序的脆弱性。 。
细节
对于许多人来说,很明显函数的参数具有不确定的评估顺序,但是这种行为与链式函数调用的交互方式可能并不那么明显。当我第一次分析此案时,这对我来说并不明显,对所有专家审阅者也似乎都不是。
乍看起来,由于每个replace
函数都必须从左到右进行评估,因此相应的函数参数组也必须作为从左到右的组进行评估。
这是不正确的,尽管函数链接确实为每个函数调用引入了从左到右的求值顺序,但是函数参数的赋值顺序不确定,每个函数调用的参量仅在它们属于成员函数调用之前才被排序的。特别是,这会影响以下调用:
s.find( "even" )
和:
s.find( " don't" )
在以下方面不确定地排序:
s.replace(0, 4, "" )
这两个find
调用可以在之前或之后进行评估replace
,这很重要,因为它的副作用s
会改变结果的方式find
,它会改变长度s
。因此,根据replace
相对于两个find
调用评估的时间,结果将有所不同。
如果我们查看链接表达式并检查某些子表达式的求值顺序:
s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
^ ^ ^ ^ ^ ^ ^ ^ ^
A B | | | C | | |
1 2 3 4 5 6
和:
.replace( s.find( " don't" ), 6, "" );
^ ^ ^ ^
D | | |
7 8 9
注意,我们忽略了这样的事实,4
并且7
可以进一步细分为更多子表达式。所以:
A
先于先B
序先于先C
序先于先序D
1
到9
被不定相对于其他子表达式用下面列出的一些例外的测序
1
到3
之前被排序B
4
到6
之前被排序C
7
到9
之前被排序D
这个问题的关键是:
关于4
和7
关于评估选择的潜在顺序B
解释了评估之间clang
和gcc
评估时结果的差异f2()
。在我的测试clang
评估B
评估之前4
并7
同时gcc
对其进行评估后。我们可以使用以下测试程序来演示每种情况下发生的情况:
#include <iostream>
#include <string>
std::string::size_type my_find( std::string s, const char *cs )
{
std::string::size_type pos = s.find( cs ) ;
std::cout << "position " << cs << " found in complete expression: "
<< pos << std::endl ;
return pos ;
}
int main()
{
std::string s = "but I have heard it works even if you don't believe in it" ;
std::string copy_s = s ;
std::cout << "position of even before s.replace(0, 4, \"\" ): "
<< s.find( "even" ) << std::endl ;
std::cout << "position of don't before s.replace(0, 4, \"\" ): "
<< s.find( " don't" ) << std::endl << std::endl;
copy_s.replace(0, 4, "" ) ;
std::cout << "position of even after s.replace(0, 4, \"\" ): "
<< copy_s.find( "even" ) << std::endl ;
std::cout << "position of don't after s.replace(0, 4, \"\" ): "
<< copy_s.find( " don't" ) << std::endl << std::endl;
s.replace(0, 4, "" ).replace( my_find( s, "even" ) , 4, "only" )
.replace( my_find( s, " don't" ), 6, "" );
std::cout << "Result: " << s << std::endl ;
}
的结果gcc
(现场观看)
position of even before s.replace(0, 4, "" ): 26
position of don't before s.replace(0, 4, "" ): 37
position of even after s.replace(0, 4, "" ): 22
position of don't after s.replace(0, 4, "" ): 33
position don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it
的结果clang
(在线观看):
position of even before s.replace(0, 4, "" ): 26
position of don't before s.replace(0, 4, "" ): 37
position of even after s.replace(0, 4, "" ): 22
position of don't after s.replace(0, 4, "" ): 33
position even found in complete expression: 22
position don't found in complete expression: 33
Result: I have heard it works only if you believe in it
的结果Visual Studio
(在线观看):
position of even before s.replace(0, 4, "" ): 26
position of don't before s.replace(0, 4, "" ): 37
position of even after s.replace(0, 4, "" ): 22
position of don't after s.replace(0, 4, "" ): 33
position don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it
标准的细节
我们知道,除非指定了子表达式的求值没有顺序,否则这是从C ++ 11标准草案“ 1.9
程序执行”部分得出的,其中说:
除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的求值是无序列的。[...]
并且我们知道,函数调用从函数节中引入了函数调用后缀表达式和参数相对于函数体的先后顺序关系1.9
:
[...]调用函数时(无论函数是否为内联函数),在执行每个表达式或语句之前,对与任何参数表达式或指定所调用函数的后缀表达式关联的每个值计算和副作用进行排序在被调用函数的主体中。[...]
我们还知道,类成员访问以及因此链接将在5.2.5
类成员访问部分中从左至右进行评估,该部分显示:
[...]计算点或箭头之前的后缀表达式;64
该求值的结果与id表达式一起确定整个后缀表达式的结果。
注意,在其中的情况下ID-表达最终被一个非静态成员函数它没有指定的评估顺序表达式列表内的()
,因为这是一个单独的子表达。来自5.2
Postfix表达式的相关语法:
postfix-expression:
postfix-expression ( expression-listopt) // function call
postfix-expression . templateopt id-expression // Class member access, ends
// up as a postfix-expression
C ++ 17的变化
提案p0145r3:完善Idiomatic C ++的表达式评估顺序进行了几处更改。通过加强postfix-expressions及其expression-list的评估规则的顺序,包括使代码具有明确指定行为的更改。
[expr.call] p5说:
postfix-expression在expression-list中的每个表达式和任何默认参数之前排序。相对于任何其他参数的初始化,不确定地排序一个参数的初始化,包括每个相关的值计算和副作用。[注意:参数评估的所有副作用都在输入函数之前先进行排序(请参见4.6)。—尾注] [示例:
void f() {
std::string s = "but I have heard it works even if you don’t believe in it";
s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don’t"), 6, "");
assert(s == "I have heard it works only if you believe in it"); // OK
}
—结束示例]
s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );