“C ++编程语言”第4版第36.3.6节中的代码是否有明确定义的行为?

时间:2014-11-26 21:02:35

标签: c++ c++11 language-lawyer operator-precedence unspecified-behavior

在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 liveunspecified

2 个答案:

答案 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可以进一步细分为更多子表达式的事实。所以:

  • 7A之前排序,BC
  • 之前排序的D之前排序
  • 19对其他子表达式进行了不确定的排序,下面列出了一些例外情况
    • 13B
    • 之前排序
    • 46C
    • 之前排序
    • 79D
    • 之前排序

这个问题的关键是:

  • 49B
  • 的排序不确定

47关于B的评估选择的潜在顺序解释了clanggcc在评估{{}时的结果差异1}}。在我的测试中,f2()在评估clangB之前评估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

C ++ 17更改

提案p0145r3: Refining Expression Evaluation Order for Idiomatic C++做了一些更改。通过加强后缀表达式及其表达式列表的评估规则的顺序,包括为代码指定良好行为的更改。

[expr.call]p5说:

  

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.1Clang 4.0.0成功编译。