【问题标题】:Does this code from "The C++ Programming Language" 4th edition section 36.3.6 have well-defined behavior?“C++ 编程语言”第 4 版第 36.3.6 节中的这段代码是否具有明确定义的行为?
【发布时间】:2015-01-25 08:25:49
【问题描述】:

在 Bjarne Stroustrup 的 The C++ Programming Language 第 4 版部分 36.3.6 类似 STL 的操作 中使用以下代码作为 chaining 的示例:

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)。

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

【问题讨论】:

  • 更好:s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
  • 除了错误,我是唯一一个认为这样丑陋的代码不应该出现在书中的人吗?
  • @KarolyHorvath 请注意,cout << a << b << coperator<<(operator<<(operator<<(cout, a), b), c) 只是稍微不那么丑。
  • @Oktalist: :) 至少我明白了那里的意图。它以简洁的格式同时教授依赖于参数的名称查找和运算符语法......它并没有给人的印象是你应该实际编写这样的代码。

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


【解决方案1】:

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

提案N4228: Refining Expression Evaluation Order for Idiomatic C++ 中提到了此示例,该提案对问题中的代码进行了以下说明:

[...]此代码已经过全球 C++ 专家的审查,并已发布 (C++ 编程语言,第 4th 版。)然而,它的漏洞 直到最近才发现未指定的评估顺序 通过工具[...]

详情

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

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

这是不正确的,函数参数具有未指定的求值顺序,尽管链接函数调用确实为每个函数调用引入了从左到右的求值顺序,但每个函数调用的参数仅在成员函数调用之前排序他们是其中的一部分。这尤其会影响以下调用:

s.find( "even" )

和:

s.find( " don't" )

关于以下方面的不确定排序:

s.replace(0, 4, "" )

两个find 调用可以在replace 之前或之后进行评估,这很重要,因为它对s 有副作用,会改变find 的结果,它会改变s。因此,取决于何时相对于两个 find 调用评估 replace,结果会有所不同。

如果我们查看链接表达式并检查某些子表达式的计算顺序:

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

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

  • AB 之前排序,在C 之前排序,在D 之前排序
  • 19 相对于其他子表达式的顺序不确定,下面列出了一些例外情况
    • 13B 之前排序
    • 46C 之前排序
    • 79D 之前排序

这个问题的关键在于:

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

47 相对于 B 的潜在评估选择顺序解释了在评估 f2()clanggcc 之间的结果差异。在我的测试中,clang 在评估47 之前评估B,而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 程序执行,它说:

除非另有说明,否则对单个运算符的操作数进行求值 和单个表达式的子表达式是无序的。[...]

我们知道函数调用引入了函数调用后缀表达式和参数相对于函数体的顺序之前的关系,来自 1.9 部分:

[...]调用函数时(无论函数是否内联),每个 与任何参数相关的值计算和副作用 表达式,或带有指定被调用者的后缀表达式 函数,在执行每个表达式之前排序或 在被调用函数的主体中声明。[...]

我们还知道,类成员访问和链接将从左到右评估,来自5.2.5类成员访问部分,其中说:

[...]计算点或箭头之前的后缀表达式;64 该评估的结果,连同 id 表达式, 确定整个后缀表达式的结果。

注意,在 id-expression 最终成为非静态成员函数的情况下,它没有指定 expression-list 的求值顺序() 因为那是一个单独的子表达式。相关语法来自5.2后缀表达式

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++ 进行了一些更改。包括通过加强 postfix-expressions 及其 expression-list 的评估规则的顺序来为代码提供明确指定行为的更改。

[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
}

——结束示例]

【讨论】:

  • 我有点惊讶地看到“许多专家”忽略了这个问题,众所周知,评估函数调用的 postfix-expression 不是先排序的评估参数(在所有版本的 C 和 C++ 中)。
  • @ShafikYaghmour 函数调用相对于彼此和其他所有内容的顺序是不确定的,除了您提到的先排序关系。但是,1、2、3、5、6、8、9、"even""don't"s 的几个实例的计算相对于彼此是无序的。
  • @TC 不,它不是(这就是这个“错误”的出现方式)。例如。 foo().func( bar() ) ,它可以在调用bar() 之前或之后调用foo()后缀表达式foo().func 。参数和后缀表达式在func() 的主体之前排序,但相对于彼此不排序。
  • @MattMcNabb 啊,对,我看错了。您说的是 postfix-expression 本身而不是调用。是的,没错,它们是无序的(当然,除非有其他规则适用)。
  • 还有一个因素是人们倾向于假设出现在 B.Stroustrup 书中的代码是正确的,否则肯定有人已经注意到了! (相关;SO 用户仍然会在 K&R 中发现新错误)
【解决方案2】:

这是为了添加有关 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++ 专家审查并发布(C++ 编程语言,第 4 版)。然而,它的对未指定评估顺序的漏洞直到最近才被工具发现。

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

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

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

以上代码使用GCC 7.1.1Clang 4.0.0编译成功。

【讨论】:

    猜你喜欢
    • 2018-12-25
    • 2018-01-23
    • 2011-06-10
    • 1970-01-01
    • 2011-08-26
    • 2014-02-07
    • 2018-01-29
    • 2016-12-04
    • 2021-10-21
    相关资源
    最近更新 更多