当前位置: 首页 > 知识库问答 >
问题:

《C编程语言》第四版第36.3.6节中的代码是否有明确的行为?

鲁才艺
2023-03-14

在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(实时查看)时不会失败。

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

共有2个答案

谷梁向荣
2023-03-14

这是为了补充有关C17的信息。C17的提案(惯用C修订版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版)。然而,直到最近,一种工具才发现它容易受到不明评价顺序的影响。

本文建议改变前C 17规则对表达式评价顺序的影响,该规则已经存在了三十多年。它建议语言应该保证当代习语,否则就会有“模糊、难以找到的错误的陷阱和来源”的风险,比如上面的代码样本。

c17的建议是要求每个表达式都有一个定义明确的求值顺序:

  • 后缀表达式从左到右求值。这包括函数调用和成员选择表达式

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

汤修贤
2023-03-14

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

提案N4228中提到了这个例子:改进惯用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可以进一步分解为更多的子表达式。所以:

  • A排在B之前,后者排在C之前,后者排在D
  • 之前
  • 19对于其他子表达式不确定排序,但下面列出了一些例外
    • 13B
    • 之前排序
    • 46C
    • 之前排序
    • 79D
    • 之前排序

    这个问题的关键在于:

    • 49相对于B

    关于B47的评估选择的潜在顺序解释了clanggcc在评估f2()时结果的差异。在我的测试中,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(请参阅直播)

    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
    

    叮当声的结果(请现场查看):

    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.5class member access一节中,类成员访问权和链接将从左到右进行评估,该节说明:

    [...]计算点或箭头之前的后缀表达式64该求值的结果与id表达式一起决定整个后缀表达式的结果。

    注意,在id表达式最终成为非静态成员函数的情况下,它不会指定()中表达式列表的求值顺序,因为这是一个单独的子表达式。5.2Postfix表达式中的相关语法:

    postfix-expression:
        postfix-expression ( expression-listopt)       // function call
        postfix-expression . templateopt id-expression // Class member access, ends
                                                       // up as a postfix-expression
    

    提案p0145r3:改进惯用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
    }
    

    -[结束示例]

 类似资料:
  • 第19.3节介绍了一章中的字符串表示法,其主要重点是运算符重载,特别是特殊运算符

  • 这段代码的问题是变量不停留在2,它递增到3。您可以在此处查看:https://wandbox.org/permlink/p5JC1nOA4pIpsgXb 我们不必使用<code>std::ref()</code>来增加这个变量。这是书中的一个错误,还是自C 11以来发生了一些变化?

  • 本文向大家介绍SQLite教程(十四):C语言编程实例代码(2),包括了SQLite教程(十四):C语言编程实例代码(2)的使用技巧和注意事项,需要的朋友参考一下 三、高效的批量数据插入:     在给出操作步骤之前先简单说明一下批量插入的概念,以帮助大家阅读其后的示例代码。事实上,批量插入并不是什么新的概念,在其它关系型数据库的C接口API中都提供了一定的支持,只是接口的实现方式不同而已。纵观众

  • 第12章 汇编语言和C语言 C/C++语言是一个被广泛使用的程序设计语言,它不仅具有良好的高级语言特征,而且还具有一些低级语言的特点,如:寄存器变量、位操作等。所以,C语言的程序与汇编语言程序之间能很平滑地衔接。另外,目前主要的C语言程序开发环境,如:Turbo C/C++、Borland C/C++等,也都提供了很好的混合编程手段。 本章主要介绍汇编语言和C语言的混合编程和调用方法。虽然其它高级

  • 1.字符串的扩展与修复 语言脚本都对字符串特别关注,有关的方法特别多,这些方法有三大类: 第一类:与标签无关的实现:charAt,charCodeAt,concat,lastIndexOf,localeCompare,match,replace,slice,split,substr,substring,toLocaleLowerCase,toLocalUpperCase,toLowerCase,t

  • C语言与汇编 汇编语言 C语言与汇编之计算机结构 C语言与汇编之用汇编写一个Helloword C语言与汇编之寄存器和寻址方式 C语言与汇编之函数调用的本质