由于子表达式求值的未指定顺序,代码表现出未指定的行为,尽管它不会调用未定义的行为,因为所有副作用都在函数内完成,在这种情况下,这会在副作用之间引入顺序关系。
提案N4228: Refining Expression Evaluation Order for Idiomatic 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
and7
可以进一步分解为更多子表达式的事实。所以:
A
排在前B
面 排在前C
面 排在前面 排在前面D
1
to9
相对于其他子表达式的顺序是不确定的,下面列出了一些例外情况
1
to3
被排序在之前B
4
to6
被排序在之前C
7
to9
被排序在之前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-expression最终成为非静态成员函数的情况下,它不会指定表达式列表的求值顺序,()
因为这是一个单独的子表达式。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: Refining Expression Evaluation Order for Idiomatic C++进行了一些更改。包括通过加强后缀表达式及其表达式列表的评估规则顺序来为代码提供明确指定行为的更改。
[expr.call]p5说:
后缀表达式在表达式列表中的每个表达式和任何默认参数之前进行排序。参数的初始化,包括每个相关的值计算和副作用,相对于任何其他参数的初始化顺序是不确定的。[注意:参数评估的所有副作用在输入函数之前排序(参见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
}
—结束示例]