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

Sha*_*our 95 c++ operator-precedence language-lawyer unspecified-behavior c++11

在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" ) ;
}
Run Code Online (Sandbox Code Playgroud)

断言失败gcc(看到它直播)和Visual Studio(看到它的实时),但它在使用Clang时没有失败(请参见实时).

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

Sha*_*our 104

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

N4228提案中提到了这个例子:为Idiomatic C++精炼表达式评估顺序,其中说明了以下关于问题中代码的内容:

[...]此代码已由全球C++专家审阅并发布(The C++ Programming Language, 4 版.)然而,它最近才被工具[...发现 - 其未经指定的评估顺序的漏洞. ]

细节

对于许多人来说,函数的参数具有未指定的评估顺序可能是显而易见的,但是这种行为与链接函数调用的交互方式可能并不明显.当我第一次分析这个案例并且显然不是所有的专家评审员时,这对我来说并不明显.

乍一看,似乎每个replace必须从左到右进行评估,相应的函数参数组也必须从左到右评估为组.

这是不正确的,函数参数具有未指定的评估顺序,尽管链接函数调用确实为每个函数调用引入了从左到右的评估顺序,每个函数调用的参数仅在成员函数调用之前排序它们是它们的一部分的.特别是这会影响以下调用:

s.find( "even" )
Run Code Online (Sandbox Code Playgroud)

和:

s.find( " don't" )
Run Code Online (Sandbox Code Playgroud)

对于以下方面不确定地排序:

s.replace(0, 4, "" )
Run Code Online (Sandbox Code Playgroud)

这两个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
Run Code Online (Sandbox Code Playgroud)

和:

.replace( s.find( " don't" ), 6, "" );
 ^        ^                   ^  ^
 D        |                   |  |
          7                   8  9
Run Code Online (Sandbox Code Playgroud)

注意,我们忽略了这样一个事实,4并且7可以进一步细分为更多的子表达式.所以:

  • A对其进行测序,然后对其B进行测序,之后C进行测序D
  • 19被不定相对于其他子表达式用下面列出的一些例外的测序
    • 13之前测序B
    • 46之前测序C
    • 79之前测序D

这个问题的关键是:

  • 4对... 9进行不确定的排序B

评价首选的潜在订单4,并7相对于B解释之间的结果的差异clanggcc评估的时候f2().在我的测试中,在clang评估B之前评估47gcc评估之后进行评估.我们可以使用以下测试程序来演示每种情况下发生的情况:

#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 ;
}
Run Code Online (Sandbox Code Playgroud)

结果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
Run Code Online (Sandbox Code Playgroud)

结果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
Run Code Online (Sandbox Code Playgroud)

结果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
Run Code Online (Sandbox Code Playgroud)

标准细节

我们知道,除非指定子表达式的评估未被排序,否则这来自草案C++ 11标准部分1.9 程序执行,其中说:

除非另有说明,否则对个体操作员的操作数和个别表达式的子表达式的评估是不确定的.[...]

并且我们知道函数调用在函数调用postfix表达式和关于函数体的参数的关系之前引入了一个序列1.9:

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

我们也知道类成员访问权限因此链接将从左到右进行评估,从5.2.5 类成员访问权限中进行评估:

[...]评估点或箭头之前的后缀表达式; 64 该评估的结果与id-expression一起确定整个后缀表达式的结果.

注意,在其中的情况下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
Run Code Online (Sandbox Code Playgroud)

C++ 17的变化

提案p0145r3: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
}
Run Code Online (Sandbox Code Playgroud)

- 末端的例子]

  • 我很惊讶地看到"很多专家"忽略了这个问题,众所周知,在评估参数之前(在C和C++的所有版本中),评估函数调用的*postfix-expression*没有排序. (7认同)
  • 还有一个因素是人们倾向于假设出现在B.Stroustrup书中的代码是正确的,否则有人肯定已经注意到了!(相关; SO用户仍然发现K&R出现新的错误) (6认同)
  • @TC不是不是(这就是"bug"的产生方式).例如`foo().func(bar())`,它可以在调用`bar()`之前或之后调用`foo()`.*postfix-expression*是`foo().func`.参数和后缀表达式在`func()`的主体之前排序,但是相对于彼此没有排序. (4认同)