这段代码定义明确吗?

发布于 2024-10-12 01:24:36 字数 777 浏览 3 评论 0原文

此代码取自此处的讨论。

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

这段代码定义明确吗? Fun() 中的 ++k 是否先于 Sun() 中的 k 计算?

如果 k 是用户定义类型而不是内置类型怎么办?上面的函数调用顺序在哪些方面与此不同:

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

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

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

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

This code is taken from a discussion going on here.

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

Is this code well-defined? Is ++k in Fun() evaluated before k in Sun()?

What if k is user-defined type, not built-in type? And in what ways the above function calls order is different from this:

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

As far as I know, in both situations, there exists a sequence point after each function call. If so, then why can't the first case is also well-defined like the second one?

Section 1.9.17 of the C++ ISO standard says this about sequence points and function evaluation:

When calling a function (whether or
not the function is inline), there is
a sequence point after the evaluation
of all function arguments
(if any)
which takes place before execution of
any expressions or statements in the
function body
. There is also a
sequence point after the copying of a
returned value and before the
execution of any expressions outside
the function
.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

追风人 2024-10-19 01:24:36

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

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

这告诉我们的不是“在对函数的参数求值之后唯一可能发生的事情是实际的函数调用”,但简单地说,在参数求值完成之后、函数调用之前的某个时刻存在一个序列点。

但如果你想象这样的情况:

foo(X).bar(Y)

这给我们的唯一保证是:

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

但这样的顺序仍然是可能的:

  1. 评估X
  2. 评估Y
  3. (将Xfoo分开的序列点call)
  4. call foo
  5. (将 Ybar 分开的序列点 call)
  6. call bar

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

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

I think if you read exactly what that standard quote says, the first case won't be well-defined:

When calling a function (whether or not the function is inline), there is a sequence point after the evaluation of all function arguments (if any) which takes place before execution of any expressions or statements in the function body

What this tells us is not that "the only thing that can happen after the arguments for a function have been evaluated is the actual function call", but simply that there is a sequence point at some point after the evaluation of arguments finishes, and before the function call.

But if you imagine a case like this:

foo(X).bar(Y)

the only guarantee this gives us is that:

  • X is evaluated before the call to foo, and
  • Y is evaluated before the call to bar.

But an order such as this would still be possible:

  1. evaluate X
  2. evalute Y
  3. (sequence point separating X from foo call)
  4. call foo
  5. (sequence point separating Y from bar call)
  6. call bar

and of course, we could also swap around the first two items, evaluating Y before X. Why not? The standard only requires that the arguments for a function are fully evaluated before the first statement of the function body, and the above sequences satisfy that requirement.

That's my interpretation, at least. It doesn't seem to say that nothing else may occur between argument evaluation and function body -- just that those two are separated by a sequence point.

节枝 2024-10-19 01:24:36

这取决于 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,则变为未定义。让我们绘制一棵采用 int< 的版本树/代码>。

                     <eval body of Fun>
                             |
                             % // pre-call sequence point
                             | 
 { S(increment, k) }  <-  E(++k) 
                             |     
                      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 产生了副作用(在最上面)不被序列点分隔:在此表达式中,相对于其他子表达式,根本不存在序列点。最底部的 % 表示完整表达序列点。

This depends on how Sun is defined. The following is well-defined

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();
}

If you change the parameter type of Sun to int, it becomes undefined. Let's draw a tree of the version taking an int.

                     <eval body of Fun>
                             |
                             % // pre-call sequence point
                             | 
 { S(increment, k) }  <-  E(++k) 
                             |     
                      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

As can be seen, we have a read of k (designated by V(k)) and a side-effect on k (at the very top) that are not separated by a sequence point: In this expression, relative to each other sub-expression, there is no sequence point at all. The very bottom % signifies the full-expression sequence point.

挽手叙旧 2024-10-19 01:24:36

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

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

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

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

This is undefined behavior, because the value of k is being both modified and read in the same expression, without an intervening sequence point. See the excellent long answer to this question.

The quote from 1.9.17 tells you that all function arguments are evaluated before the body of the function is called, but doesn't say anything about the relative order of evaluation of arguments to different function calls within the same expression -- no guarantee that "++k Fun() is evaluated before k in Sun()".

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

is different because the ; is a sequence point, so the order of evaluation is well-defined.

权谋诡计 2024-10-19 01:24:36

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

#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

...with -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 这样的旧编译器):同意,但这是一个有用的健全性检查

As a little test, consider:

#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);
}

I run this with gcc 3.4.6 and no optimisation and get:

5
4
3
2

...with -O3...

2
3
4
5

So, either that version of 3.4.6 had a major bug (which is a bit hard to believe), or the sequence is undefined as Philip Potter suggested. (GCC 4.1.1 with/without -O3 produced 5, 5, 5, 5.)

EDIT - my summary of the discussion in comments below:

  • 3.4.6 really might have had a bug (well, yes)
  • many newer compilers happen to produce 5/5/5/5... is that a defined behaviour?
    • probably not, as it corresponds to all increment side effects being "actioned" before any of the function calls are made, which is not a behaviour that anyone here has suggested could be guaranteed by the Standard
  • this isn't a very good approach to investigating the Standard's requirements (particularly with an older compiler like 3.4.6): agreed, but it's a useful sanity check
不及他 2024-10-19 01:24:36

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

我已将 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在线没有)


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

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

I know that the behavior of compilers cannot really prove anything, but I thought it would be interesting to check out what the internal representation of a compiler would give (still a bit higher level than assembly inspection).

I've used the Clang/LLVM online demo with this code:

#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
}

And compiled with the standard optimizations (in C++ mode), it gave:

/tmp/webcompile/_13371_0.cc: In function 'int main(int, char**)':
/tmp/webcompile/_13371_0.cc:16: warning: operation on 'i' may be undefined

which I did find interesting (did any other compiler warned about this ? Comeau online did not)


As an aside it also produced the following Intermediate Representation (scroll to the right):

@.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
}

Apparently, Clang behaves like gcc 4.x.x does and first evaluates all arguments before performing any function call.

从来不烧饼 2024-10-19 01:24:36

一个有点老的问题,但这使它更有趣,因为正确的答案是:
这取决于!

当这个问题最初于 2011 年提出时,答案显然是否定的! someInstance.Fun(++k).Gun(10).Sun(k).Tun(); 是未指定的行为,导致变量 k 被修改并读取同样的表情。

但在 C++17 中,情况发生了变化:
https://wg21.link/p0145
这样,函数的参数计算被指定为在任何进一步的链式函数调用之前进行排序。
这意味着现在 someInstance.Fun(++k).Gun(10).Sun(k).Tun(); 计算顺序是明确定义且明确的:
someInstance>> ++k>>乐趣()>> 10>>枪()>> k>>太阳()>>敦()

A bit older question but this makes it more interesting cause the correct answer is:
It depends!

When the question was originally asked in 2011 the answer was clearly NO! someInstance.Fun(++k).Gun(10).Sun(k).Tun(); is unspecified behaviour cause the variable k is modified an read in the same expression.

But with C++17 this was changed:
https://wg21.link/p0145
With this the argument-evaluation of a function was specified as being sequenced-before any further chained function-calls.
This means that now someInstance.Fun(++k).Gun(10).Sun(k).Tun(); the evaluation-order is well-defined and unambiguous:
someInstance >> ++k >> Fun() >> 10 >> Gun() >> k >> Sun() >> Tun()

心头的小情儿 2024-10-19 01:24:36

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

The second case is certainly well-defined. A string of tokens that ends with a semicolon is an atomic statement in C++. Each statement is parsed, processed and completed before the next statement is begun.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文