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

85

考虑本主题作为以下主题的续集:

前一篇文章
未定义行为和序列点

让我们重新审视这个 有趣复杂 的表达式(斜体短语来自上述主题 *微笑*):

i += ++i;
我们说这会引发未定义行为。我认为当我们这样说时,我们隐含地假设i类型是内置类型之一。
如果i类型是用户定义的类型呢?比如它的类型是在本文稍后定义的Index(见下文)。这是否仍会引发未定义行为?
如果是,为什么?这不等同于写成i.operator+=(i.operator++());甚至更简单的语法i.add(i.inc());吗?或者,它们也会产生未定义行为吗?
如果不是,为什么?毕竟,在连续序列点之间,i对象被修改了两次。请记住经验法则:一个表达式只能在连续的“序列点”之间修改一次对象的值。如果i += ++i是一个表达式,那么它必须引发未定义行为。如果是这样,那么它的等效表达式i.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;
        }
};

13
+1 很棒的问题,引发了很好的回答。我觉得我应该说一下,这仍然是可怕的代码,应该重构为更易读的形式,但您可能已经知道了 :) - Philip Potter
4
问题是:谁说它们是相同的?或者谁说它们不相同?这不取决于您如何实现它们吗?(注意:我假设s的类型是用户定义的类型!) - Nawaz
5
在两个序列点之间,我没有看到任何标量对象被修改了两次... - Johannes Schaub - litb
3
@Johannes: 然后涉及到“标量(scalar)”对象,它是什么?我想知道为什么我以前从未听说过它。也许是因为教程/C++-faq没有提及或强调它?它是否与“内置(built-in)”类型的对象不同? - Nawaz
3
@Phillip: 显然,在实际生活中我不会写这样的代码;事实上,没有一个理智的程序员会写这种代码。通常设计这些问题是为了更好地理解未定义行为和序列点的整个业务! :-) - Nawaz
显示剩余6条评论
5个回答

49

看起来代码有

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

在序列点方面,它的表现非常正常。C++ ISO标准1.9.17节关于序列点和函数评估如下:

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

这意味着,例如,在将i.operator ++()作为operator +=的参数时,其评估后有一个序列点。总之,由于重载运算符是函数,因此正常的排序规则适用。

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


12

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

有一个问题,+++++i在C++03中是否定义良好? - Industrial-antidepressant

11

就像其他人所说,你的 i += ++i 示例可以使用用户定义类型,因为你调用了函数,而函数包含序列点。

然而,a[++i] = i 就不太幸运了,假设 a 是你基本的数组类型,甚至是一个用户定义类型。你面临的问题是我们不知道包含 i 的表达式的哪一部分会首先被求值。可能是 ++i 被求值后,传递给 operator[](或原始版本),以便检索那里的对象,然后 i 的值被传递给它(在 i 加 1 之后)。另一方面,也可能是后面的部分先被求值,存储以供稍后赋值,然后求解 ++i 部分。


@Philip:未指定意味着我们期望编译器指定行为,而未定义则没有这样的义务。我认为在这里是未定义的,以便让编译器有更多的优化空间。 - Matthieu M.
1
@Philip:结果是UB,因为5/4规则中的规定:“对于完整表达式的每个可允许的子表达式顺序都必须满足这一段的要求;否则行为未定义。”如果所有可允许的顺序在修改++i和赋值右手边读取i之间有序列点,那么顺序将是未指定的。因为其中一个可允许的顺序在没有中间序列点的情况下执行了这两件事,所以行为是未定义的。 - Steve Jessop
1
@Philip: 它并不仅将未指定行为定义为未定义行为。再次强调,如果未指定行为的范围包括一些未定义的行为,那么整体行为就是未定义的。如果未指定行为的范围在所有可能性上被定义,那么整体行为就是未指定的。但你在第二点上是对的,我正在考虑用户定义的 a 和内置的 i - Steve Jessop
@supercat:如果你的foo++是完整表达式的子表达式,那么听起来是正确的。那么,“只要不再允许其他代码”就是前一个序列点,“在允许其他代码之前”的时间就是下一个序列点。在那个“时间”里,你可以把foo视为被占用的对象,而这个子表达式是唯一可以访问它的表达式的部分。 - Steve Jessop
我明白为什么 a[++i] = i 不行。那 a[(++i)] = i 呢? - gsamaras
显示剩余8条评论

8
我认为这很明确:
根据C++标准草案(n1905)§1.9/16:
“在复制返回值之后,在函数外执行任何表达式之前,还有一个序列点13)。C++中的几个上下文会导致函数调用的评估,即使在翻译单元中没有相应的函数调用语法。[例如:新表达式的评估调用一个或多个分配和构造函数;参见5.3.4。另一个例子是,在没有函数调用语法出现的情况下,转换函数(12.3.2)的调用可能出现在某些上下文中。- 结束示例]函数入口和函数退出处的序列点(如上所述)是函数调用作为评估的特征,无论调用函数的表达式的语法是什么。”
请注意我加粗的部分。这意味着在增量函数调用(i.operator ++())之后,但在复合赋值调用(i.operator + =)之前确实存在一个序列点。

6

好的。看完之前的回复,我重新思考了自己的问题,尤其是这一部分:只有Noah尝试回答这个问题,但我并不完全信服他。

a[++i] = i;

案例1:

如果a是内置类型的数组。那么Noah说得对。也就是说,

即使是你的基本数组类型或者甚至是用户定义的类型,a[++i] = i都不会那么幸运。问题在于我们不知道包含i的表达式中哪一部分会首先被计算。

因此,a[++i]=i会引发未定义的行为,或者结果是未指定的。无论它是什么,它都没有被很好地定义!

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

案例2:

如果a是用户定义类型的对象,并且重载了operator[],那么又有两种情况。

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

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

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


1
@Nawaz k++k之间没有序列点分隔。在评估SunFun之前,它们都可以被评估。语言仅要求在评估Sun的参数之前评估Fun,而不是在评估Sun的参数之前评估Fun的参数。我正在重新解释同样的事情,但无法提供参考资料,所以我们无法从这里继续。 - Philip Potter
1
@Nawaz:因为没有定义一个序列点来分隔它们。在Sun执行之前和之后都有序列点,但是Fun的参数++k可能在此之前或之后被评估。在Fun执行之前和之后都有序列点,但是Sun的参数k可能在此之前或之后被评估。因此,一种可能的情况是,在SunFun被评估之前,k++k都被评估,因此两者都在函数调用序列点之前,因此没有序列点分隔k++k - Philip Potter
1
@Philip:我再说一遍:这种情况与 eat(i++);drink(10);sleep(i); 有什么不同?即使现在,你也可以说 i++ 在那之前或之后被评估? - Nawaz
1
@Nawaz:我怎样才能更清楚地表达自己?在Fun/Sun示例中,k++k之间没有序列点。在eat/drink示例中,ii++之间序列点。 - Philip Potter
3
@Philip:这根本没有意义。在Fun()和Sun()之间存在一个序列点,但它们的参数之间不存在序列点。这就像说,在eat()sleep()之间存在序列点(s),但它们的参数之间甚至没有一个序列点。两个函数调用的参数如何在序列点之间分隔,属于相同的序列点呢? - Nawaz
显示剩余13条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接