这段代码是否定义明确?

时间:2011-01-17 03:17:38

标签: c++ operator-precedence sequence-points

此代码取自here上的讨论。

someInstance.Fun(++k).Gun(10).Sun(k).Tun();

这段代码是否定义明确? Fun()中的++k是否在Sun()中的k之前进行了评估?

如果k是用户定义的类型,而不是内置类型,该怎么办?以上函数调用顺序的方式与此不同:

eat(++k);drink(10);sleep(k);

据我所知,在两种情况下,每次函数调用后都存在序列点 。如果是这样,为什么第一个案例也不能像第二个案例那样明确定义?

C ++ ISO标准的第1.9.17节对序列点和功能评估进行了说明:

  

调用函数时(无论是否为   不是函数是内联的),有   评估后的序列点   所有函数参数(如果有的话)   在执行之前发生   中的任何表达或陈述   功能体。还有一个   复制后的序列点   返回值和之前   在外面执行任何表达式   功能

6 个答案:

答案 0 :(得分:22)

我认为如果您完全阅读 标准引语所说的内容,那么第一种情况就不会明确定义:

  

当调用函数时(无论函数是否为内联函数),在评估函数体中执行任何表达式或语句之前发生的所有函数参数(如果有)之后都有一个序列点

这告诉我们的不是“在评估函数的参数之后唯一可能发生的事情是实际的函数调用”,而只是在参数评估结束后的某个时刻有一个序列点,在函数调用之前。

但如果你想象一个这样的案例:

foo(X).bar(Y)

这给我们的唯一保证就是:

    在调用X和之前评估
  • foo 在致电Y之前评估
  • bar

但是这样的命令仍然是可能的:

  1. 评估X
  2. 评估Y
  3. (序列点将Xfoo电话分开)
  4. 致电foo
  5. (序列点将Ybar电话分开)
  6. 致电bar
  7. 当然,我们也可以交换前两项,在Y之前评估X。为什么不?该标准仅要求在函数体的第一个语句之前完全评估函数的参数,并且上述序列满足该要求。

    这至少是我的解释。似乎没有说在参数评估和函数体之间可能出现 nothing else - 只是那两个被序列点分开。

答案 1 :(得分:12)

这取决于Sun的定义方式。以下是定义明确的

struct A {
  A &Fun(int);
  A &Gun(int);
  A &Sun(int&);
  A &Tun();
};

void g() {
  A someInstance;
  int k = 0;
  someInstance.Fun(++k).Gun(10).Sun(k).Tun();
}

如果您将Sun的参数类型更改为int,则它将变为未定义。我们draw a tree的版本为int

                     <eval body of Fun>
                             |
                             % // pre-call sequence point
                             | 
 { S(increment, k) }  <-  E(++x) 
                             |     
                      E(Fun(++k).Gun(10))
                             |
                      .------+-----.       .-- V(k)--%--<eval body of Sun>
                     /              \     /
                   E(Fun(++k).Gun(10).Sun(k))
                              |
                    .---------+---------. 
                   /                     \ 
                 E(Fun(++k).Gun(10).Sun(k).Tun())
                              |
                              % // full-expression sequence point

从可以看出,我们读取了k(由V(k)指定)和k(在最顶部)的副作用,这些副作用未被a分隔序列点:在此表达式中,相对于彼此的子表达式,根本没有序列点。最底部%表示全表达序列点。

答案 2 :(得分:10)

这是未定义的行为,因为k的值在同一表达式中被修改和读取,没有插入序列点。请参阅this question的优秀长答案。

1.9.17中的引用告诉您在调用函数体之前评估所有函数参数,但没有说明在同一表达式中对不同函数调用的参数求值的相对顺序 - 不能保证在Sun()中的k之前评估“++ k Fun()”。

eat(++k);drink(10);sleep(k);

是不同的,因为;是一个序列点,所以评估的顺序是明确定义的。

答案 3 :(得分:8)

作为一个小测试,请考虑:

#include <iostream>

struct X
{
    const X& f(int n) const
    {
        std::cout << n << '\n';
        return *this;
    }
};

int main()
{
    int n = 1;

    X x;

    x.f(++n).f(++n).f(++n).f(++n);
}

我用gcc 3.4.6运行它并且没有优化并得到:

5
4
3
2

...与-O3 ......

2
3
4
5

所以,3.4.6版本的版本有一个主要的错误(有点难以置信),或者正如菲利普波特所暗示的那样,序列未定义。 (GCC 4.1.1有/无-O3产生5,5,5,5。)

编辑 - 我在下面的评论中对讨论的总结:

  • 3.4.6确实可能有一个错误(好吧,是的)
  • 许多较新的编译器恰好产生了5/5/5/5 ......这是一个定义的行为吗?
    • 可能不是,因为它对应于在进行任何函数调用之前被“操作”的所有增量副作用,这不是标准所保证的任何人都可以保证的行为
  • 这不是一个非常好的方法来调查标准的要求(特别是对于像3.4.6这样的旧编译器):同意,但这是一个有用的理智检查

答案 4 :(得分:1)

我知道编译器的行为无法真正证明什么,但我认为检查编译器的内部表示会给出什么(仍然比汇编检查更高一级)会很有趣。

我已将Clang/LLVM online demo与此代码一起使用:

#include <stdio.h>
#include <stdlib.h>

struct X
{
  X const& f(int i) const
  {
    printf("%d\n", i);
    return *this;
  }
};

int main(int argc, char **argv) {
  int i = 0;
  X x;
  x.f(++i).f(++i).f(++i);         // line 16
}

使用标准优化(在C ++模式下)编译,它给出了:

  

/tmp/webcompile/_13371_0.cc:在函数'int main(int,char **)'中:
  /tmp/webcompile/_13371_0.cc:16:警告:'i'上的操作可能未定义

我确实觉得有趣(其他任何编译器都警告过这个吗?Comeau在线没有)


另外,它还产生了以下中间表示(向右滚动):

@.str = private constant [4 x i8] c"%d\0A\00", align 1 ; <[4 x i8]*> [#uses=1]

define i32 @main(i32 %argc, i8** nocapture %argv) nounwind {
entry:
  %0 = tail call i32 (i8*, ...)* @printf(i8* noalias getelementptr inbounds ([4 x i8]* @.str, i64 0, i64 0), i32 3) nounwind ; <i32> [#uses=0]
                                                                                                             ^^^^^
  %1 = tail call i32 (i8*, ...)* @printf(i8* noalias getelementptr inbounds ([4 x i8]* @.str, i64 0, i64 0), i32 3) nounwind ; <i32> [#uses=0]
                                                                                                             ^^^^^
  %2 = tail call i32 (i8*, ...)* @printf(i8* noalias getelementptr inbounds ([4 x i8]* @.str, i64 0, i64 0), i32 3) nounwind ; <i32> [#uses=0]
                                                                                                             ^^^^^
  ret i32 0
}

显然,Clang的行为与gcc 4.x.x一样,并且在执行任何函数调用之前首先计算所有参数。

答案 5 :(得分:0)

第二种情况肯定是明确定义的。以分号结尾的一串标记是C ++中的原子语句。在下一个语句开始之前,每个语句都会被解析,处理和完成。