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


84

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

上一期文章
未定义行为和顺序点

让我们重新看一下这个有趣令人费解的表达(斜体字词来自上面的主题* smile *):

i += ++i;

我们说这会调用未定义的行为。我相信,当这样说,我们隐含假设i是内置的类型之一。

如果什么类型i是用户定义类型?说它的类型是本文Index后面定义的类型(请参阅下文)。它仍然会调用未定义行为吗?

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

如果没有,为什么不呢?毕竟,对象在连续的序列点之间i被修改了两次。请回想一下经验法则:表达式只能在连续的“序列点”之间修改对象的值一次。如果 i += ++i是表达式,则它必须调用undefined-behavior。如果是,则它的等效项i.operator+=(i.operator++());i.add(i.inc());必须调用undefined-behavior,似乎是不正确的!(据我了解)

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

如果它是一个表达式,并且其行为也得到了很好的定义,则意味着与该表达式关联的序列点数在某种程度上取决于该表达式所涉及的操作数的类型。我是否正确(甚至部分正确)?


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

//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 2011年

3
@Johannes:那是关于标量对象的。它是什么?我想知道为什么我以前从未听说过它。也许是因为tutorials / C ++-faq没有提到它,或者不强调它?它与内置类型的对象不同吗?
Nawaz

3
@Phillip:显然,我不会在现实生活中编写此类代码;实际上,没有理智的程序员会编写它。通常设计这些问题是为了使我们能够更好地理解不确定行为和顺序点的全部内容!:-)
Nawaz

Answers:


48

看起来像代码

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

关于顺序点,效果很好。C ++ ISO标准的1.9.17节说明了有关顺序点和功能评估的内容:

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

例如,这将指示i.operator ++()作为参数operator +=的评估之后具有序列点。简而言之,由于重载运算符是函数,因此适用常规排序规则。

顺便问一个好问题!我真的很喜欢您如何迫使我理解我已经以为自己知道(并且以为我以为自己知道)的语言的所有细微差别。:-)



11

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

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


那么...由于表达式的计算顺序未指定,因此结果是否未指定而不是UB?
菲利普·波特

@Philip:未指定意味着我们希望编译器指定行为,而未定义则没有这种义务。我认为这里是未定义的,目的是让编译器有更多的优化空间。
Matthieu M.

@Noah:我也发表了回应。请检查一下,让我知道您的想法。:-)
Nawaz

1
@Philip:由于5/4中的规则,结果为UB:“对于完整表达式的子表达式的每个允许顺序,都应满足本段的要求;否则行为是不确定的。” 如果所有允许的排序在修改++i和读取i分配的RHS之间都具有序列点,则该顺序将不确定。由于允许的顺序之一在没有中间顺序点的情况下完成了这两项操作,因此行为是不确定的。
史蒂夫·杰索普

1
@Philip:它不仅将未指定的行为定义为未定义的行为。同样,如果未指定行为的范围包括一些未定义的行为,总体行为是未定义的。如果未指定行为的范围在所有可能性中均已定义,整体行为将未被指定。但是,您在第二点上是对的,我想到的是用户定义a和内置的i
史蒂夫·杰索普

8

我认为这是明确的:

根据C ++草案标准(n1905)§1.9/ 16:

“复制返回的值之后,在执行函数外部的任何表达式之前,还有一个序列点。13。即使转换单元中没有相应的函数调用语法,C ++中的多个上下文也会导致函数调用的求值。 [实施例:新的表达式的求值调用一个或多个分配和构造函数;见5.3.4对于另一个例子,一个转换功能(12.3.2)的调用可以在背景中出现的其中没有函数调用的语法出现。 -端示例]无论表达式的语法如何,函数入口和函数出口的序列点(如上所述)都是所求值的函数调用的特征调用该函数可能是。”

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


6

好的。经过先前的回答后,我重新考虑了自己的问题,特别是在这一部分中,只有诺亚(Noah)试图回答,但我并不完全相信他。

a[++i] = i;

情况1:

Ifa是内置类型的数组。那挪亚说的是对的。那是,

a [++ i] =假设a是您的基本数组类型,我不是很幸运, 甚至是用户定义的 。您在这里遇到的问题是,我们不知道包含i的表达式的哪一部分首先被求值。

因此,a[++i]=i调用undefined-behavior,或者结果不确定。无论是什么,它都没有明确定义!

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

情况2:

如果a是用户定义类型的对象,该对象使过载operator[],则再次有两种情况。

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

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

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


1
@Nawazk++k通过序列点分离。他们既可以之前评估要么Sun或者Fun进行评估。该语言要求Fun在之前进行评估Sun,而不要求Fun在的参数之前进行评估Sun。我有点无法提供参考就再次解释了同一件事,因此我们不会从这里开始。
菲利普·波特

1
@Nawaz:因为没有定义分隔它们的顺序点的东西。在Sun执行之前和之后都有序列点,但是Fun的参数++k可以在执行之前或之后进行求值。在Fun执行之前和之后都有序列点,但是Sun的参数k可以在执行之前或之后进行求值。因此,一种可能的情况是,k和和++k都在Sun或之前Fun被求值,因此两者都在函数调用序列点之前,因此没有分隔点k和的序列点++k
菲利普·波特

1
@Philip:我再说一遍:这种情况有何不同eat(i++);drink(10);sleep(i);?...即使是现在,您可以说i++可能在此之前或之后进行评估?
Nawaz

1
@Nawaz:我如何使自己更加清晰?在Fun / Sun示例中,和之间没有序列点。在饮食示例中,和之间有一个序列点。k++kii++
菲利普·波特

3
@Philip:那根本没有道理。在Fun()和Sun()之间存在一个序列点,但是在它们的参数之间不存在序列点。就像说,在eat()和之间sleep()存在序列点,但是在那之间参数甚至都不存在。用序列点分隔的两个函数调用的参数如何属于相同的序列点?
Nawaz
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.