94

在 Bjarne Stroustrup 的The C++ Programming Language 4th edition 部分36.3.6 STL-like Operations中,使用以下代码作为链接示例:

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( see it live ) 和Visual Studio( see it live ) 中失败,但在使用Clang ( see it live )时它不会失败。

为什么我得到不同的结果?这些编译器中是否有任何一个错误地评估了链接表达式,或者此代码是否表现出某种形式的未指定未定义的行为

4

2 回答 2

105

由于子表达式求值的未指定顺序,代码表现出未指定的行为,尽管它不会调用未定义的行为,因为所有副作用都在函数内完成,在这种情况下,这会在副作用之间引入顺序关系。

提案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产生副作用,它会改变. 因此,根据相对于两个调用的评估时间,结果会有所不同。sfindsreplacefind

如果我们查看链接表达式并检查一些子表达式的评估顺序:

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

请注意,我们忽略了4and7可以进一步分解为更多子表达式的事实。所以:

  • A排在前B面 排在前C面 排在前面 排在前面D
  • 1to9相对于其他子表达式的顺序是不确定的,下面列出了一些例外情况
    • 1to3被排序在之前B
    • 4to6被排序在之前C
    • 7to9被排序在之前D

这个问题的关键在于:

  • 49不确定的顺序相对于B

评估选择的潜在顺序4解释7了评估B之间clanggcc评估时结果的差异f2()。在我的测试中,在clang评估B之前进行评估47而在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
}

—结束示例]

于 2014-11-26T21:02:34.740 回答
4

这是为了添加有关 C++17 的信息。用于解决引用上述代码的问题的提案(为惯用 C++ 修订版 2 优化表达式评估顺序)作为样本。C++17

按照建议,我从提案中添加了相关信息并引用(突出显示我的):

标准中目前规定的表达式求值顺序破坏了建议、流行的编程习惯或标准库设施的相对安全性。陷阱不仅适用于新手或粗心的程序员。即使我们知道规则,它们也会不分青红皂白地影响我们所有人。

考虑以下程序片段:

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++ 专家审查并发布(C++ 编程语言,第 4 版)。然而,它对未指定的评估顺序的脆弱性直到最近才被工具发现。

这篇论文建议改变受到影响并存在了三十多年C++17的关于表达评估顺序的预规则。C它建议该语言应保证当代习语或冒“陷阱和来源晦涩难懂,难以发现错误”的风险,例如上面代码示例所发生的情况。

的提议C++17要求每个表达式都有一个明确定义的评估顺序

  • 后缀表达式从左到右计算。这包括函数调用和成员选择表达式。
  • 赋值表达式从右到左求值。这包括复合作业。
  • 移位运算符的操作数从左到右求值。
  • 涉及重载运算符的表达式的求值顺序由与相应内置运算符关联的顺序决定,而不是函数调用的规则。

GCC 7.1.1上面的代码使用and成功编译Clang 4.0.0

于 2017-06-12T11:02:31.467 回答