在Bjarne Stroustrup的 The C++ Programming Language 第4版部分36.3.6
类STL操作中,以下代码用作{{3}的示例}:
void f2()
{
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" ) ;
}
断言在gcc
( chaining )和Visual Studio
( see it live )中失败,但确实如此使用see it live( Clang )时不会失败。
为什么我会得到不同的结果?这些编译器是否错误地评估了链接表达式,或者此代码是否显示某种形式的see it live或unspecified?
答案 0 :(得分:104)
由于未指定的子表达式评估顺序,代码表现出未指定的行为,尽管它没有调用未定义的行为,因为在这种情况下副作用之间的所有副作用都在函数which introduces a sequencing relationship内完成。
提案N4228: Refining Expression Evaluation Order for Idiomatic C++中提到了此示例,其中提到了以下有关问题代码的内容:
[...]此代码已由全球C ++专家审核并发布 (C ++编程语言,4 th 版。)然而,它的漏洞 最近才发现了未指定的评估顺序 通过工具[...]
详细
对于许多人来说,函数的参数具有未指定的评估顺序可能是显而易见的,但这种行为与链接函数调用的交互方式可能并不那么明显。当我第一次分析这个案例并且显然不是所有的专家评论员时,这对我来说并不明显。
乍一看,由于每个replace
必须从左到右进行评估,因此必须从左到右评估相应的函数参数组。
这是不正确的,函数参数具有未指定的评估顺序,尽管链接函数调用确实为每个函数调用引入了从左到右的求值顺序,每个函数调用的参数仅在成员函数调用之前排序他们是一部分。特别是这会影响以下调用:
s.find( "even" )
和
s.find( " don't" )
对于以下方面不确定地排序:
s.replace(0, 4, "" )
可以在find
之前或之后评估两个replace
调用,这很重要,因为它会对s
产生副作用,从而改变{{1}的结果}},它会更改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
至9
对B
4
和7
关于B
的评估选择的潜在顺序解释了clang
和gcc
在评估{{}时的结果差异1}}。在我的测试中,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
( 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
程序执行,其中包含:
除非另有说明,否则评估各个运营商的操作数 个别表达的子表达式没有序列。[...]
我们知道函数调用在函数调用postfix表达式和关于函数体的参数之前引入了一个序列,来自1.9
部分:
[...]当调用一个函数(函数是否为内联函数)时,每一个 值计算和与任何参数相关的副作用 表达式,或使用指定被调用的后缀表达式 函数,在执行每个表达式之前排序或 被调用函数体内的陈述。[...]
我们也知道,类成员访问权限以及因此链接将从左到右评估,从5.2.5
类成员访问部分开始评估:
[...]评估点或箭头之前的后缀表达式; 64 该评估的结果与id-expression一起, 确定整个后缀表达式的结果。
注意,在 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
提案p0145r3: Refining Expression Evaluation Order for Idiomatic C++做了一些更改。通过加强后缀表达式及其表达式列表的评估规则的顺序,包括为代码指定良好行为的更改。
postfix-expression在表达式列表中的每个表达式和任何默认参数之前进行排序。该 参数的初始化,包括每个相关的值计算和副作用,是不确定的 相对于任何其他参数的顺序排序。 [注意:论证评估的所有副作用是 在输入功能之前排序(见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 }
-end example]
答案 1 :(得分:4)
这是为了添加有关C ++ 17的相关信息。 C++17
的提案(Refining Expression Evaluation Order for Idiomatic C++ Revision 2)解决了上述代码的问题,并将其作为标本。
正如所建议的那样,我从提案中添加了相关信息并引用(突出显示):
表达式评估的顺序,正如目前在标准中指定的那样,破坏了建议,流行的编程习惯用语或标准库设施的相对安全性。陷阱不只是新手 或粗心的程序员。即使我们知道规则,它们也会不分青红皂白地影响我们所有人。
考虑以下程序片段:
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"); }
断言应该验证程序员的预期结果。它使用成员函数调用的“链接”,这是一种常见的标准实践。此代码已由全球C ++专家审阅并发布(The C ++ Programming Language,第4版。)然而,它最近才被工具发现未指定评估顺序的漏洞。 / p>
该论文建议改变受C++17
影响的表达评估顺序的前C
规则,并且已存在超过三十年。它提出语言应该保证当代成语或冒险“陷阱和晦涩难懂的来源,很难找到错误”,例如上面代码标本发生的事情。
C++17
的提案是要求每个表达式都有明确定义的评估顺序:
上述代码使用GCC 7.1.1
和Clang 4.0.0
成功编译。