ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

让我们重温一下这个有趣而复杂的表达方式(斜体词组取自上述主题 *smile* ):

i += ++i;

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

如果 itype 是用户定义的类型怎么办?假设它的类型是 Index,它在本文后面定义(见下文)。它还会调用未定义的行为吗?

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

如果没有,为什么不呢?毕竟,对象 i 在连续序列点之间被修改两次。请记住经验法则:an expression can modify an object's value only once between consecutive "sequence points。如果 i += ++i 是一个表达式,那么它必须调用 undefined-behavior。如果是这样,那么它的等价物 i.operator+=(i.operator++());i.add(i.inc()); 也必须调用 undefined-behavior 这似乎是不真实的! (据我所理解)

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

t
templatetypedef

它看起来像代码

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

在序列点方面工作得很好。 C++ ISO 标准的第 1.9.17 节对序列点和函数求值进行了说明:

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

例如,这将表明作为 operator += 的参数的 i.operator ++() 在其评估后具有一个序列点。简而言之,因为重载运算符是函数,所以适用正常的排序规则。

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


I
Industrial-antidepressant

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 中定义良好?
4
4xy

正如其他人所说,您的 i += ++i 示例适用于用户定义的类型,因为您正在调用函数,并且函数包含序列点。

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


所以......结果是未指定而不是UB,因为表达式的评估顺序未指定?
@Philip:未指定意味着我们希望编译器指定行为,而未定义则没有这样的义务。我认为这里没有定义,让编译器有更多的优化空间。
@Noah:我也发布了回复。请检查一下,让我知道你的想法。 :-)
@Philip:结果是 UB,因为 5/4 中的规则:“对于完整表达式的子表达式的每个允许排序,应满足本段的要求;否则行为未定义。”。如果所有允许的顺序在修改 ++i 和分配的 RHS 上的 i 读数之间都有序列点,那么顺序将是未指定的。因为允许的排序之一在没有干预序列点的情况下完成这两项操作,所以行为是未定义的。
@Philip:它不仅将未指定的行为定义为未定义的行为。同样,如果未指定行为的范围包括一些未定义的行为,整体行为未定义。 如果在所有可能性中都定义了未指定行为的范围,那么整体行为是未指定的。但是您在第二点上是对的,我在考虑用户定义的 a 和内置的 i
M
Matthew Flaschen

我认为它定义明确:

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

“在复制返回值之后和执行函数外的任何表达式之前还有一个序列点 13)。C++ 中的几个上下文会导致对函数调用进行评估,即使在翻译单元中没有出现相应的函数调用语法。 [ 示例:新表达式的求值调用一个或多个分配和构造函数;参见 5.3.4。另一个示例,转换函数 (12.3.2) 的调用可能出现在没有函数调用语法出现的上下文中。-结束示例] 函数入口和函数出口处的序列点(如上所述)是函数调用的特征,无论调用函数的表达式的语法是什么。

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


C
Community

好吧。看完之前的回答后,我重新思考了自己的问题,特别是只有诺亚试图answer但我并不完全相信他的部分。

a[++i] = i;

情况1:

如果 a 是一个内置类型的数组。那么诺亚说的是对的。那是,

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

所以 a[++i]=i 调用 undefined-behavior,或者结果未指定。不管它是什么,它都不是很好定义的!

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

案例二:

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

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

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

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


@Nawaz k++k 由序列点分隔。它们都可以在评估 SunFun 之前进行评估。 only 语言要求在 Sun 之前评估 Fun,而不是在 Sun 的参数之前评估 Fun 的参数。我有点想再次解释同样的事情,但无法提供参考,所以我们不会从这里开始。
@Nawaz:因为没有定义将它们分开的序列点。在 Sun 执行之前和之后有序列点,但 Fun 的参数 ++k 可以在此之前或之后进行评估。在 Fun 执行之前和之后有序列点,但 Sun 的参数 k 可以在此之前或之后进行评估。因此,一种可能的情况是 k++k 都在 SunFun 被评估之前被评估,因此两者都在函数调用序列点之前,因此没有序列点分隔 { 6} 和 ++k
@Philip:我再说一遍:这种情况与 eat(i++);drink(10);sleep(i); 有何不同? ...即使是现在,您也可以说 i++ 可能会在此之前或之后进行评估?
@Nawaz:我怎样才能让自己更清楚?在 Fun/Sun 示例中,k++k 之间没有 no 序列点。在吃/喝示例中,有 is 作为 ii++ 之间的序列点。
@Philip:这根本没有意义。在 Fun() 和 Sun() 之间存在一个序列点,但在它们的参数之间不存在序列点。就像在说,在 eat()sleep() 之间存在序列点,但在它们之间甚至没有一个参数。由序列点分隔的两个函数调用的参数如何属于 same 序列点?