27

此代码取自此处正在进行的讨论。

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

这段代码定义明确吗?之前在 Sun( ) 中++k评估过 Fun( )吗?k

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

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

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

C++ ISO 标准的第 1.9.17 节对序列点和函数求值进行了说明:

调用函数时(无论该函数是否内联), 在对所有函数参数(如果有的话) 求值之后都会有一个序列点,该序列点发生在函数体中的任何表达式或语句执行之前。在复制返回值之后和执行函数外的任何表达式之前还有一个 序列点

4

6 回答 6

22

我认为,如果您准确阅读该标准引用的内容,则第一种情况将无法明确定义:

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

这告诉我们的不是“在对函数的参数进行评估之后唯一可能发生的事情就是实际的函数调用”,而只是在对参数的评估完成之后和之前的某个时间点存在一个序列点函数调用。

但是,如果您想象这样的情况:

foo(X).bar(Y)

这给我们的唯一保证是:

  • X在调用 之前进行评估foo,并且
  • Y在调用bar.

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

  1. 评估X
  2. 评估Y
  3. X(与foo调用分离的序列点)
  4. 称呼foo
  5. Y(与bar调用分离的序列点)
  6. 称呼bar

当然,我们也可以交换前两项,评估Ybefore X。为什么不?该标准只要求函数的参数在函数体的第一条语句之前完全评估,并且上述序列满足该要求。

这是我的解释,至少。似乎并没有说参数评估和函数体之间不会发生其他任何事情——只是这两者被一个序列点隔开。

于 2011-01-17T03:51:19.663 回答
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();
}

如果您将参数类型更改Sunint,它将变为未定义。让我们绘制一个版本树,采用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(在最顶部),它们没有被序列点分隔:序列点。最底部%表示全表达序列点。

于 2011-01-17T13:16:35.473 回答
10

这是未定义的行为,因为 k 的值在同一个表达式中被修改和读取,没有中间的序列点。查看出色的长答案请参阅此问题

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

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

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

于 2011-01-17T03:27:45.950 回答
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 有一个重大错误(这有点难以置信),要么正如菲利普波特所建议的那样,序列未定义。(带有/不带 -O3 的 GCC 4.1.1 产生 5、5、5、5。)

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

  • 3.4.6 真的可能有一个错误(嗯,是的)
  • 许多较新的编译器碰巧产生 5/5/5/5 ......这是定义的行为吗?
    • 可能不是,因为它对应于在进行任何函数调用之前被“执行”的所有增量副作用,这不是这里任何人都建议可以由标准保证的行为
  • 这不是调查标准要求的好方法(特别是对于像 3.4.6 这样的旧编译器):同意,但这是一个有用的健全性检查
于 2011-01-17T03:37:19.880 回答
1

我知道编译器的行为不能真正证明任何事情,但我认为检查编译器的内部表示会给出什么会很有趣(仍然比汇编检查高一点)。

我使用了Clang/LLVM 在线演示和以下代码:

#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 online 没有)


顺便说一句,它还产生了以下中间表示(向右滚动):

@.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.xx 一样,在执行任何函数调用之前首先评估所有参数。

于 2011-01-17T08:04:55.863 回答
0

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

于 2011-01-17T03:28:30.467 回答