从“The C ++ Programming Language”第四版第36.3.6节的这段代码是否有明确的行为?
在Bjarne Stroustrup的“C ++编程语言”第4版第36.3.6
节36.3.6
STL操作中 ,以下代码被用作链接的一个例子:
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
( 现场看到 )和Visual Studio
( 现场看到它 )中失败了,但是在使用Clang ( 现场看到它 )的时候并没有失败。
为什么我会得到不同的结果? 这些编译器是否有错误地评估链接expression式,或者这些代码是否performance出某种forms的未指定或未定义的行为 ?
由于子expression式的评估顺序没有指定,所以代码performance出未指定的行为,尽pipe它没有调用未定义的行为,因为所有副作用都是在引入副作用之间的顺序关系的函数内完成的。
提案N4228:提炼用于习惯C ++的expression式评估顺序中提到了这个例子,其中提到了以下关于问题中的代码:
[…]这个代码已经被全世界的C ++专家审查过,并且发表了(C ++编程语言, 第 4版)。然而,它对于未指定顺序的评估的脆弱性最近才被一个工具发现[..]。 ]
细节
很多人可能很清楚,函数的参数具有未指定的评估顺序,但是这种行为与链接函数调用的交互作用可能并不明显。 当我第一次分析这个案子时,对我来说并不明显,显然也不是所有的专家审查员 。
乍一看可能会出现,因为每个replace
都必须从左到右进行评估,所以相应的函数参数组也必须从左到右评估为组。
这是不正确的,函数参数具有未指定的评估顺序,尽pipe链接函数调用确实为每个函数调用引入了从左到右的评估顺序,每个函数调用的参数仅在成员函数调用之前被sorting,的。 特别是这影响了以下呼叫:
s.find( "even" )
和:
s.find( " don't" )
它们在以下方面被不确定地sorting:
s.replace(0, 4, "" )
这两个find
调用可以在replace
之前或之后进行评估,这很重要,因为它对s
有副作用,会改变find
的结果,它会改变s
的长度。 所以取决于什么时候replace
被评估相对于两个find
调用结果将有所不同。
如果我们看链式expression式,并检查一些子expression式的评估顺序:
s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" ) ^ ^ ^ ^ ^ ^ ^ ^ ^ AB | | | C | | | 1 2 3 4 5 6
和:
.replace( s.find( " don't" ), 6, "" ); ^ ^ ^ ^ D | | | 7 8 9
请注意,我们忽略了4
和7
可以进一步分解成更多子expression式的事实。 所以:
-
A
在B
之前进行测序,在D
之前对C
进行测序 - 对于其他子expression式,
1
到9
是不确定的,除了下面列出的一些例外- 在
B
之前1
到3
被sorting - 在
C
之前4
到6
被sorting - 在
D
之前9
被sorting
- 在
这个问题的关键在于:
- 对于
B
4
到9
是不确定的
对于B
和B
的评估select的潜在顺序解释了在评估f2()
时clang
与gcc
之间的结果的差异。 在我的testing中, clang
在评估4
和7
之前评估B
,而gcc
评估之后评估它。 我们可以使用下面的testing程序来演示每种情况下发生的事情:
#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
从标准的细节
我们知道,除非指定,否则子expression式的评估是不确定的,这是来自C ++ 11标准第1.9
节的程序执行 ,它说:
除了注意到的地方之外,对个别操作符和个别expression式的操作数的评估是不确定的。
我们知道一个函数调用在函数调用postfixexpression式和参数之间的关系之前引入了一个顺序,
[…]当调用一个函数(函数是否内联)时,在每个expression式或语句执行之前,每个与任何参数expression式相关联的值计算和副作用,或者指定被调用函数的后缀expression式都被sorting在被调用函数的主体中。
我们也知道,类的成员访问,因此链接将从左到右评估,从5.2.5
节的类成员访问中说:
[…]计算点或箭头之前的后缀expression式; 64这个评估结果和idexpression式一起决定了整个后缀expression式的结果。
注意,在idexpression式最终是一个非静态成员函数的情况下,它并不指定()
expression式列表的求值顺序,因为这是一个单独的子expression式。 5.2
后缀expression式的相关语法:
postfix-expression: postfix-expression ( expression-listopt) // function call postfix-expression . templateopt id-expression // Class member access, ends // up as a postfix-expression
这是为了增加有关C ++ 17的信息。 C++17
的提议( 精炼expression式评估顺序为习惯C ++修订2 )解决了引用上面的代码的问题作为标本。
正如所build议的那样,我添加了提案中的相关信息并引用(突出显示了我的):
expression评估的顺序,正如目前在标准中所规定的那样,破坏了build议,stream行的编程习惯用语,或标准图书馆设施的相对安全性。 陷阱不只是新手或粗心的程序员。 即使我们知道规则,他们也会不分青红皂白地影响我们所有人。
考虑下面的程序片段:
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"); }
该断言应该validation程序员的预期结果。 它使用成员函数调用的“链接”,这是一个通用的标准做法。 这个代码已经被全世界的C ++专家所审查,并且被发表(The C ++ Programming Language,4th edition)。然而,它对于未指定评估顺序的脆弱性最近才被一种工具发现。
本文提出改变C++17
规则,使其受C
影响的expression评估顺序已经存在了三十多年。 它提出, 语言应该保证当代成语,或者冒着“晦涩难懂,难以发现错误的陷阱和来源”等风险。
C++17
的build议是要求每个expression式都有一个定义良好的评估顺序 :
- 后缀expression式从左到右进行评估。 这包括函数调用和成员selectexpression式。
- 赋值expression式从右到左计算。 这包括复合分配。
- 移位运算符的操作数从左到右进行计算。
- 涉及重载运算符的expression式的求值顺序由与相应的内build运算符相关的顺序决定,而不是函数调用的规则。
上面的代码使用GCC 7.1.1
和Clang 4.0.0
编译成功。