由于子表达式的求值顺序未指定,代码表现出未指定的行为,尽管它不会调用未定义的行为,因为在这种情况下,所有副作用都在函数which introduces a sequencing relationship 内完成。
提案N4228: Refining Expression Evaluation Order for Idiomatic C++ 中提到了此示例,该提案对问题中的代码进行了以下说明:
[...]此代码已经过全球 C++ 专家的审查,并已发布
(C++ 编程语言,第 4th 版。)然而,它的漏洞
直到最近才发现未指定的评估顺序
通过工具[...]
详情
对于许多人来说,函数的参数具有未指定的评估顺序可能很明显,但这种行为如何与链式函数调用交互可能并不那么明显。当我第一次分析这个案例时,这对我来说并不明显,显然对所有专家审阅者也不是。
乍一看,由于每个replace 都必须从左到右进行评估,因此相应的函数参数组也必须作为从左到右的组进行评估。
这是不正确的,函数参数具有未指定的求值顺序,尽管链接函数调用确实为每个函数调用引入了从左到右的求值顺序,但每个函数调用的参数仅在成员函数调用之前排序他们是其中的一部分。这尤其会影响以下调用:
s.find( "even" )
和:
s.find( " don't" )
关于以下方面的不确定排序:
s.replace(0, 4, "" )
两个find 调用可以在replace 之前或之后进行评估,这很重要,因为它对s 有副作用,会改变find 的结果,它会改变s。因此,取决于何时相对于两个 find 调用评估 replace,结果会有所不同。
如果我们查看链接表达式并检查某些子表达式的计算顺序:
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 的潜在评估选择顺序解释了在评估 f2() 时 clang 和 gcc 之间的结果差异。在我的测试中,clang 在评估4 和7 之前评估B,而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 的结果 (see it live)
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 的结果(see it live):
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 的结果(see it live):
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
标准中的细节
我们知道,除非指定,否则子表达式的评估是无序的,这是来自 draft C++11 standard 部分 1.9 程序执行,它说:
除非另有说明,否则对单个运算符的操作数进行求值
和单个表达式的子表达式是无序的。[...]
我们知道函数调用引入了函数调用后缀表达式和参数相对于函数体的顺序之前的关系,来自 1.9 部分:
[...]调用函数时(无论函数是否内联),每个
与任何参数相关的值计算和副作用
表达式,或带有指定被调用者的后缀表达式
函数,在执行每个表达式之前排序或
在被调用函数的主体中声明。[...]
我们还知道,类成员访问和链接将从左到右评估,来自5.2.5类成员访问部分,其中说:
[...]计算点或箭头之前的后缀表达式;64
该评估的结果,连同 id 表达式,
确定整个后缀表达式的结果。
注意,在 id-expression 最终成为非静态成员函数的情况下,它没有指定 expression-list 的求值顺序() 因为那是一个单独的子表达式。相关语法来自5.2后缀表达式:
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++ 进行了一些更改。包括通过加强 postfix-expressions 及其 expression-list 的评估规则的顺序来为代码提供明确指定行为的更改。
[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
}
——结束示例]