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


94

在Bjarne Stroustrup的C ++编程语言第4版第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实时显示)时它不会失败。

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


更好:s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
Ben Voigt 2014年

20
除了错误,我是唯一一个不应该在书中这样认为丑陋代码的人吗?
Karoly Horvath

5
@KarolyHorvath注意cout << a << b << coperator<<(operator<<(operator<<(cout, a), b), c)只是勉强不太难看。
Oktalist,2014年

1
@Oktalist::)至少我有意图。它以简洁的格式同时讲授与参数相关的名称查找和运算符语法...并且它没有给人以您实际上应该编写类似代码的印象。
Karoly Horvath

Answers:


104

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

提案N4228:完善惯用C ++的表达式评估顺序中提到了此示例,其中对问题中的代码说了以下内容:

[...]此代码已由世界各地的C ++专家审查并发布(《 C ++编程语言》, 4 版。)然而,仅在最近一种工具中才发现其对未指定评估顺序的脆弱性。 。

细节

对于许多人来说,很明显函数的参数具有不确定的评估顺序,但是这种行为与链式函数调用的交互方式可能并不那么明显。当我第一次分析此案时,这对我来说并不明显,对所有专家审阅者也似乎都不是。

乍看起来,由于每个replace函数都必须从左到右进行评估,因此相应的函数参数组也必须作为从左到右的组进行评估。

这是不正确的,尽管函数链接确实为每个函数调用引入了从左到右的求值顺序,但是函数参数的赋值顺序不确定,每个函数调用的参量仅在它们属于成员函数调用之前才被排序的。特别是,这会影响以下调用:

s.find( "even" )

和:

s.find( " don't" )

在以下方面不确定地排序:

s.replace(0, 4, "" )

这两个find调用可以在之前或之后进行评估replace,这很重要,因为它的副作用s会改变结果的方式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
  • 19被不定相对于其他子表达式用下面列出的一些例外的测序
    • 13之前被排序B
    • 46之前被排序C
    • 79之前被排序D

这个问题的关键是:

  • 49被不定相对于测序B

关于47关于评估选择的潜在顺序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-表达最终被一个非静态成员函数它没有指定的评估顺序表达式列表内的(),因为这是一个单独的子表达。来自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:完善Idiomatic C ++的表达式评估顺序进行了几处更改。通过加强postfix-expressions及其expression-list的评估规则的顺序,包括使代码具有明确指定行为的更改。

[expr.call] p5说:

postfix-expression在expression-list中的每个表达式和任何默认参数之前排序。相对于任何其他参数的初始化,不确定地排序一个参数的初始化,包括每个相关的值计算和副作用。[注意:参数评估的所有副作用都在输入函数之前先进行排序(请参见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
}

—结束示例]


7
看到“许多专家”忽略了这个问题,我感到有些惊讶,众所周知,在评估参数之前(在C和C ++的所有版本中)评估函数调用的后缀表达式没有排序。
MM 2014年

@ShafikYaghmour函数调用相对于彼此以及其他所有对象不确定地排序,除了您注意到的先序关系。然而,评价1,2,3,5,6,8,9, "even""don't"和的几个实例s是未测序相对于彼此。
TC

4
@TC不,不是(这是“错误”产生的方式)。例如foo().func( bar() ),它可以在调用foo()之前或之后调用bar()。的后缀表达式foo().func。参数和postfix-expression在的主体之前排序func(),但彼此之间未排序。
MM

@MattMcNabb啊,对,我读错了。您在谈论的是postfix-expression本身,而不是调用。是的,没错,它们没有顺序(当然,除非有其他规则适用)。
TC

6
还有一个因素,人们倾向于认为出现在B.Stroustrup书中的代码是正确的,否则肯定有人已经注意到了!(相关; SO用户仍然在K&R中仍然发现新错误)
MM

4

旨在添加有关C ++ 17的信息。引用上述代码来解决该问题的提案(针对Idiomatic 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和成功编译Clang 4.0.0

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.