未定义的行为和重新加载的序列点

Undefined behavior and sequence points reloaded

提问人:Nawaz 提问时间:1/9/2011 最后编辑:CommunityNawaz 更新时间:1/20/2021 访问量:13318

问:

将本主题视为以下主题的续集:

上一期
未定义的行为和序列点

让我们重温一下这个有趣复杂的表达方式(斜体短语取自上面的主题*微笑*):

i += ++i;

我们说这调用了未定义的行为。我假设,当这样说时,我们隐含地假设 type of 是内置类型之一。i

如果类型是用户定义的类型,该怎么办?假设它的类型是本文后面定义的类型(见下文)。它是否仍会调用未定义的行为?iIndex

如果是,为什么?难道它不等同于写作,甚至在语法上更简单吗?或者,它们是否也调用了未定义的行为?i.operator+=(i.operator++());i.add(i.inc());

如果不是,为什么不呢?毕竟,对象在连续的序列点之间被修改了两次。请记住经验法则:表达式只能在连续的“序列点”之间修改对象的值一次。如果是一个表达式,那么它必须调用 undefined-behavior。如果是这样,那么它的等价物也必须调用似乎不真实的未定义行为!(据我了解)ii += ++ii.operator+=(i.operator++());i.add(i.inc());

或者,一开始就不是一个表达式吗?如果是这样,那么它是什么,表达的定义是什么?i += ++i

如果它是一个表达式,同时它的行为也是明确定义的,那么它意味着与表达式关联的序列点数在某种程度上取决于表达式中涉及的操作数的类型。我是否正确(甚至部分正确)?


顺便问一下,这个表情怎么样?

//Consider two cases:
//1. If a is an array of a built-in type
//2. If a is user-defined type which overloads the subscript operator!

a[++i] = i; //Taken from the previous topic. But here type of `i` is Index.

你也必须在你的回应中考虑这一点(如果你肯定知道它的行为)。:-)


++++++i;

在 C++03 中定义良好?毕竟,这是这个,

((i.operator++()).operator++()).operator++();

class Index
{
    int state;

    public:
        Index(int s) : state(s) {}
        Index& operator++()
        {
            state++;
            return *this;
        }
        Index& operator+=(const Index & index)
        {
            state+= index.state;
            return *this;
        }
        operator int()
        {
            return state;
        }
        Index & add(const Index & index)
        {
            state += index.state;
            return *this;
        }
        Index & inc()
        {
            state++;
            return *this;
        }
};
C 未定义行为 C++-FAQ 序列点

评论

13赞 Philip Potter 1/9/2011
+1 个好问题,激发了很好的答案。我觉得我应该说它仍然是可怕的代码,应该重构以使其更具可读性,但您可能知道无论如何:)
4赞 Nawaz 1/9/2011
@What问题是:谁说是一样的?还是谁说不一样?这难道不取决于你如何实现它们吗?(注意:我假设 type of 是用户定义的类型!s
5赞 Johannes Schaub - litb 1/9/2011
我没有看到任何标量对象在两个序列点之间被修改两次......
3赞 Nawaz 1/9/2011
@Johannes :那么它是关于标量对象的。这是什么?我想知道为什么我以前从未听说过它。也许,因为教程/C++-faq 没有提到它,或者没有强调它?它与内置类型的对象不同吗?
3赞 Nawaz 1/9/2011
@Phillip : 显然,我不会在现实生活中编写这样的代码;事实上,没有一个理智的程序员会写它。这些问题通常是为了更好地理解未定义行为和序列点的整个业务而设计的!:-)

答:

49赞 templatetypedef 1/9/2011 #1

它看起来像代码

i.operator+=(i.operator ++());

在序列点方面工作得很好。C++ ISO 标准的第 1.9.17 节对序列点和函数计算是这样说的:

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

例如,这将指示 as 参数 to 在其计算后有一个序列点。简而言之,由于重载运算符是函数,因此适用正常的排序规则。i.operator ++()operator +=

顺便说一句,好问题!我真的很喜欢你强迫我理解一种语言的所有细微差别,我已经认为我知道(并认为我认为我知道)。:-)

8赞 Matthew Flaschen 1/9/2011 #2

我认为它的定义很明确:

摘自 C++ 标准草案 (n1905) §1.9/16:

“之后还有一个序列点 复制返回值和 在执行任何之前 函数外部的表达式13)。 C++ 中的多个上下文导致 函数调用的评估,甚至 虽然没有相应的函数调用 语法出现在翻译中 单位。[ 示例:评估新的 expression 调用一个或多个 分配和构造函数; 参见 5.3.4。再举一个例子, 转换函数的调用 (12.3.2) 可能出现在 未显示函数调用语法。 — 结束示例 ] 序列指向 function-entry 和 function-exit(如 如上所述)是 函数调用已评估,无论 表达式的语法 调用函数可能是。"

请注意我加粗的部分。这意味着在增量函数调用 () 之后、复合赋值调用 () 之前确实存在一个序列点。i.operator ++()i.operator+=

11赞 Edward Strange 1/9/2011 #3

正如其他人所说,您的示例使用用户定义类型,因为您正在调用函数,而函数由序列点组成。i += ++i

另一方面,假设这是您的基本数组类型,甚至是用户定义的数组类型,那就不那么幸运了。您在这里遇到的问题是,我们不知道首先计算包含的表达式的哪一部分。它可能是被评估的,传递给(或原始版本)以便在那里检索对象,然后将 的值传递给该对象(之后是递增的)。另一方面,也许先评估后一面,存储以备以后分配,然后评估该部分。a[++i] = iai++ioperator[]ii++i

评论

0赞 Philip Potter 1/9/2011
所以。。。因此,由于表达式的计算顺序未指定,因此结果是否未指定而不是 UB?
0赞 Matthieu M. 1/9/2011
@Philip:unspecified 意味着我们希望编译器指定行为,而 undefined 则没有这样的义务。我认为这里没有定义,让编译器有更多的优化空间。
0赞 Nawaz 1/9/2011
@Noah : 我也发了回复。请检查一下,让我知道你的想法。:-)
1赞 Steve Jessop 1/12/2011
@Philip:结果是UB,因为5/4中的规则:“对于完整表达式的子表达式的每个允许顺序,应满足本款的要求;否则,行为是未定义的。如果所有允许的排序在修改和读取分配的 RHS 之间都有序列点,则该顺序将未指定。因为其中一个允许的排序在没有干预序列点的情况下执行这两件事,所以行为是未定义的。++ii
1赞 Steve Jessop 1/12/2011
@Philip:它不仅将未指定的行为定义为未定义的行为。同样,如果未指定行为的范围包括一些未定义的行为,整体行为是未定义的。如果在所有可能性中都定义了未指定行为的范围,未指定整体行为。但你在第二点上是对的,我正在考虑一个用户定义的和内置的.ai
6赞 Nawaz 1/9/2011 #4

好。在浏览了之前的回答之后,我重新思考了我自己的问题,尤其是只有诺亚试图回答的这一部分,但我并不完全相信他。

a[++i] = i;

案例一:

If 是内置类型的数组。那么挪亚说的是对的。那是a

a[++i] = 假设我没有那么幸运 a 是您的基本数组类型, 或者 甚至用户定义了一个.问题 你在这里是我们不知道的 表达式的哪一部分 首先评估包含 I。

因此调用未定义的行为,或者结果未指定。不管它是什么,它都没有明确的定义!a[++i]=i

PS:在上面的引文中,删除线当然是我的。

案例二:

如果是重载 的用户定义类型的对象,则同样有两种情况。aoperator[]

  1. 如果重载函数的返回类型是内置类型,则再次调用 undefined-behavior 或未指定结果。operator[]a[++i]=i
  2. 但是,如果重载函数的返回类型是用户定义的类型,那么 的行为是明确定义的(据我所知),因为在这种情况下等价于编写与 相同的 。也就是说,在返回的对象上调用赋值,这似乎定义得非常好,因为到返回时已经计算过了,然后返回的对象调用函数,将更新的值作为参数传递给它。请注意,这两个调用之间有一个序列点。语法确保这两个调用之间没有竞争,并且会首先被调用,然后,传递给它的参数也会首先被评估。operator[]a[++i] = ia[++i]=ia.operator[](++i).operator=(i);a[++i].operator=(i);operator=a[++i]a[++i]++ioperator=ioperator[]++i

可以将其视为每个连续的函数调用返回某个用户定义类型的对象。在我看来,这种情况更像是这样:,因为在这两种情况下,每次函数调用后都存在序列点。someInstance.Fun(++k).Gun(10).Sun(k).Tun();eat(++k);drink(10);sleep(k)

如果我错了,请纠正我。:-)

评论

1赞 Philip Potter 1/10/2011
@Nawaz,并且由序列点分隔。它们都可以在评估之前进行评估,也可以在评估之前进行评估。该语言要求 that 在 之前被求值,而不是 的参数在 的参数之前被求值。我又在解释同样的事情,但无法提供参考,所以我们不会从这里开始。k++kSunFunFunSunFunSun
1赞 Philip Potter 1/10/2011
@Nawaz:因为没有任何东西可以定义将它们分开的序列点。在执行之前和之后都有序列点,但 的参数可以在此之前或之后进行计算。在执行之前和之后都有序列点,但 的参数可以在此之前或之后进行计算。因此,一种可能的情况是,在计算 OR 之前计算 AND ,因此两者都在函数调用序列点之前,因此没有分隔 和 的序列点。SunFun++kFunSunkk++kSunFunk++k
1赞 Nawaz 1/10/2011
@Philip : 我再说一遍 : 这种情况与 ?...即使是现在,你也可以说可能在那之前或之后进行评估?eat(i++);drink(10);sleep(i);i++
1赞 Philip Potter 1/10/2011
@Nawaz:我怎样才能让自己更清晰?在 Fun/Sun 示例中,和 之间没有序列点。在 eat/drink 示例中,在 和 之间序列点。k++kii++
3赞 Nawaz 1/10/2011
@Philip:这完全没有意义。在 Fun() 和 Sun() 之间存在一个序列点,但在它们的参数之间不存在序列点。这就像说,在 和 之间存在序列点,但其中甚至没有一个参数。由序列点分隔的两个函数调用的参数如何属于相同的序列点?eat()sleep()
12赞 Industrial-antidepressant 1/10/2011 #5

http://www.eelis.net/C++/analogliterals.xhtml我想起了模拟文字

  unsigned int c = ( o-----o
                     |     !
                     !     !
                     !     !
                     o-----o ).area;

  assert( c == (I-----I) * (I-------I) );

  assert( ( o-----o
            |     !
            !     !
            !     !
            !     !
            o-----o ).area == ( o---------o
                                |         !
                                !         !
                                o---------o ).area );

评论

0赞 Industrial-antidepressant 1/10/2011
有一个问题,是++++++i;在 C++03 中定义良好?