从“The C ++ Programming Language”第四版第36.3.6节的这段代码是否有明确的行为?

在Bjarne Stroustrup的“C ++编程语言”第4版第36.3.636.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 

请注意,我们忽略了47可以进一步分解成更多子expression式的事实。 所以:

  • AB之前进行测序,在D之前对C进行测序
  • 对于其他子expression式, 19是不确定的,除了下面列出的一些例外
    • B之前13被sorting
    • C之前46被sorting
    • D之前9被sorting

这个问题的关键在于:

  • 对于B 49是不确定的

对于BB的评估select的潜在顺序解释了在评估f2()clanggcc之间的结果的差异。 在我的testing中, clang在评估47之前评估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.1Clang 4.0.0编译成功。