【问题标题】:Is this code well-defined?这段代码定义明确吗?
【发布时间】:2011-06-10 05:33:36
【问题描述】:

此代码取自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 节对序列点和函数求值进行了说明:

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

【问题讨论】:

  • 程序语言有很多坑和坑,对于一些你不确定的东西,避免它们......也许这个问题可以在 C++ 规范中回答,或者不是......
  • @Nawaz :看来我错了。 @jalf 提出了一个有效的观点。删除我的答案。
  • 行为未定义,因为“k 的访问先于它的修改”(不知道我是怎么错过的 :( )。顺便说一句,托尼的代码在 g++、Clang/IntelC++ 上给出了不同的输出。另外我什至得到g++ 上的警告(对 k 的操作可能未定义)。
  • 保留您的答案 IMO。我不是 100% 确定我的,所以我认为我们最好让两个答案都可见
  • 现在要赶往大学。返回后会单独发布更全面的答案。 :-)

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


【解决方案1】:

这取决于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(在最顶部)的副作用,它们没有被序列点分隔:在这个表达式中,相对于其他子表达式,根本没有序列点。最底部的% 表示全表达序列点。

【讨论】:

  • "取决于...以下是明确定义的"... 因为 Sun 看到的 k 值是 Sun 运行时的当前值,而不是预先准备的快照。但是,这说明了一种避免问题的方法,而不是提供对问题的洞察力。我认为是否定义了问题的场景并不“依赖” - 它只是未定义。
  • @Tony 你对我没有任何意义。
  • @Johannes :我喜欢这篇文章。 +1。顺便说一句,我想,你想说的是 “这取决于 Sun 是如何定义的”,而不是 “这取决于 Gun 已定义”。因为只有这样才对我有意义。我说的对吗?
  • @Johannes :这意味着,如果 k 是用户定义的类型(例如,here 定义的类型 Index),那么链函数调用将是明确定义的。正确的?请看案例2here
  • @Tony 我没有说int 版本的排序/排序取决于任何东西。我说:“这段代码定义好了吗?” -> “这取决于”。
【解决方案2】:

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

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

【讨论】:

  • 我尝试过的一种变体是用 argc 初始化 i ...然后它无法在编译时计算出结果,但你应该看到三个增量 - 可能会变成类似于 @ 987654324@....
  • @Tony:很好的变化,它在一定程度上有效地规避了常量折叠。
【解决方案3】:

作为一个小测试,考虑:

#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 有一个重大错误(这有点难以置信),要么如 Philip Potter 所建议的那样,序列未定义。 (带有/不带 -O3 的 GCC 4.1.1 产生 5、5、5、5。)

编辑 - 我对以下 cmets 讨论的总结:

  • 3.4.6 确实可能有错误(嗯,是的)
  • 许多较新的编译器碰巧产生 5/5/5/5...这是一种已定义的行为吗?
    • 可能不会,因为它对应于在进行任何函数调用之前“执行”所有增量副作用,这不是此处任何人建议的行为可以由标准保证
  • 这不是调查标准要求的好方法(尤其是对于像 3.4.6 这样的旧编译器):同意,但这是一个有用的健全性检查

【讨论】:

  • +1 优秀的经验答案。我没想到找到不同的输出会这么容易。
  • @Tony :你为什么要投票? :P 这篇文章没有证实任何东西 :D
  • @Tony :我建议在 gcc 3.4.6 之前使用 gcc 4.1.1 显示详细输出,因为人们会得到错误的印象:|..他们倾向于认为序列是未定义的gcc 4.1.1 及更高版本似乎不是这样!
  • @Nawaz“编译器 Y 的版本 X 恰好产生了我期望的输出”not 是否等同于“这不是未定义的”。
  • @Tony, @Nawaz: Getting 5 5 5 5, 1 2 3 4 5, 5 4 3 2 1. 有所有与它一致的开始未定义。
【解决方案4】:

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

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

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

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

【讨论】:

    【解决方案5】:

    我认为,如果您完全了解该标准引用所说的内容,则第一种情况不会被明确定义:

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

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

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

    foo(X).bar(Y)
    

    这给我们的唯一保证是:

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

    但是这样的订单仍然是可能的:

    1. 评估X
    2. 评估Y
    3. (将X 与foo 通话分开的序列点)
    4. 致电foo
    5. (将Y 与bar 通话分开的序列点)
    6. 致电bar

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

    这是我的解释,至少。似乎并没有说在参数评估和函数体之间可能会发生 nothing —— 只是这两者被一个序列点隔开。

    【讨论】:

    • @jalf :您的解释似乎是正确的,但是如果您说“这是唯一的解释”..那么到目前为止,这对我来说并不令人信服..
    • @Nawaz:也许,但请记住,如果标准没有另外说明,那么它是未定义的行为。如果我的解释是可能,如果我们找不到任何排除它的东西,那么它“默认”是未定义的。如果代码定义明确,那么标准中一定有某些内容,在 1.9.17 或其他地方,这与我的解释相矛盾。
    • @jalf :我同意。 +1 做这个笔记... :-)
    • 问题是它比你想象的还要古怪。如果 X 和 Y 不平凡(即有子表达式)。来自 X 和 Y 的子表达式的求值顺序是不相关的,因此可以交错(只是为了增加乐趣)。
    • @Nawaz:你似乎真的希望这个被定义......你是在工作中输掉赌注还是什么? ;-)
    【解决方案6】:

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

    【讨论】:

    • 嗯...通常在运行任何内容之前解析所有语句。但我想我明白你的意思,分号是确定动作顺序的一种非常可靠的方式。
    • 我并不是说语句在运行时被解析。从程序状态的角度来看——所有变量的值——每个分号定义了一个自包含的执行单元,这样每个执行单元(连同之前的所有单元)都完全定义了单元末尾的程序状态。
    猜你喜欢
    • 2011-08-26
    • 1970-01-01
    • 2023-03-30
    • 2015-09-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多