此代码取自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节对序列点和功能评估进行了说明:
调用函数时(无论是否为 不是函数是内联的),有 评估后的序列点 所有函数参数(如果有的话) 在执行之前发生 中的任何表达或陈述 功能体。还有一个 复制后的序列点 返回值和之前 在外面执行任何表达式 功能。
答案 0 :(得分:22)
我认为如果您完全阅读 标准引语所说的内容,那么第一种情况就不会明确定义:
当调用函数时(无论函数是否为内联函数),在评估函数体中执行任何表达式或语句之前发生的所有函数参数(如果有)之后都有一个序列点
这告诉我们的不是“在评估函数的参数之后唯一可能发生的事情是实际的函数调用”,而只是在参数评估结束后的某个时刻有一个序列点,在函数调用之前。
但如果你想象一个这样的案例:
foo(X).bar(Y)
这给我们的唯一保证就是:
X
和之前评估foo
在致电Y
之前评估bar
。但是这样的命令仍然是可能的:
X
Y
X
与foo
电话分开)foo
Y
与bar
电话分开)bar
当然,我们也可以交换前两项,在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。)
编辑 - 我在下面的评论中对讨论的总结:
答案 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 ++中的原子语句。在下一个语句开始之前,每个语句都会被解析,处理和完成。