代码之家  ›  专栏  ›  技术社区  ›  Alexey Malistov

为什么'i=++i+1'是未指定的行为?

  •  44
  • Alexey Malistov  · 技术社区  · 15 年前

    考虑下面的C++标准ISO/IEC 1488∶2003(E)引用(第5节第4段):

    除非另有说明,否则 个体操作数的评价 个体的运算符和子表达式 表达式,以及 发生了哪些副作用? 未指定的53)之前 下一个序列点是一个标量 对象应具有其存储值 最多修改一次 表达式的计算。 此外,优先值应为 仅用于确定值 存储。这方面的要求 应符合各段落的要求。 允许的排序 完整表达式的子表达式; 否则该行为是未定义的。 例:

    i = v[i++];  // the behavior is unspecified 
    i = 7, i++, i++;  //  i becomes 9 
    
    i = ++i + 1;  // the behavior is unspecified 
    i = i + 1;  // the value of i is incremented 
    

    “结束示例”]

    我很惊讶 i = ++i + 1 给出未定义的值 i . 有没有人知道编译器实现不提供 2 对于以下情况?

    int i = 0;
    i = ++i + 1;
    std::cout << i << std::endl;
    

    问题是 operator= 有两个ARG。第一个总是 参考文献。 在这种情况下,评估的顺序并不重要。 除了C++标准禁忌之外,我没有看到任何问题。

    拜托 考虑这样的情况:论点的顺序对评估很重要。例如, ++i + i 显然是未定义的。请只考虑我的情况 i=++i+1 .

    为什么C++标准禁止这样的表达式?

    15 回复  |  直到 13 年前
        1
  •  62
  •   Rob Kennedy    15 年前

    你犯了一个错误 operator= 作为双参数函数 ,其中参数的副作用必须在函数开始之前完全评估。如果是这样,那么表达式 i = ++i + 1 会有多个序列点,并且 ++i 将在分配开始前进行完全评估。但事实并非如此。正在评估的内容 内在的 分配运算符 ,不是用户定义的运算符。表达式中只有一个序列点。

    这个 结果 属于 +i 在赋值之前(和加法运算符之前)进行计算,但 副作用 不一定立即应用。结果 ++i + 1 总是和 i + 2 ,所以这是分配给 i 作为赋值运算符的一部分。结果 +i 总是 i + 1 所以这就是分配给 作为增量运算符的一部分。没有序列点来控制应该首先分配哪个值。

    由于代码违反了“在上一个序列点和下一个序列点之间,标量对象的存储值应通过表达式的计算最多修改一次”的规则,因此行为是未定义的。 实际 不过,很可能 I+ 1 I+ 2 将首先分配,然后分配其他值,最后程序将继续正常运行,没有鼻腔恶魔或爆炸性厕所,并且没有 i + 3 也可以。

        2
  •  37
  •   CB Bailey    15 年前

    这是未定义的行为,而不是(只是)未指定的行为,因为有两个写入 i 没有中间的序列点。就标准所规定的定义而言,就是这样。

    该标准允许编译器生成延迟回写到存储器的代码,或者从另一个角度,对实现副作用的指令重新排序,只要它符合序列点的要求就可以选择任何方式。

    这个语句表达式的问题是它意味着两次写入 没有中间序列点:

    i = i++ + 1;
    

    一次写入的值等于 “加一”,另一个是“加一”的值。在标准允许的范围内,这些写入可能以任何顺序发生,或者完全崩溃。理论上,这甚至给了实现并行执行写回的自由,而不必费心检查同时发生的访问错误。

        3
  •  15
  •   Charles Salvia    15 年前

    C/C++定义了一个概念 sequence points 这是指执行中的一个点,在这个点上可以保证先前评估的所有效果都已经执行。说 i = ++i + 1 未定义,因为它递增 i 以及分配 对自身而言,两者都不是单独定义的序列点。因此,这是不明确的,将首先发生。

        4
  •  10
  •   Johannes Schaub - litb    13 年前

    C++ 11(09/30/2011)的更新

    停止 ,这是 定义良好的 在C++ 11中。它仅在C++ 03中是未定义的,但是C++ 11更灵活。

    int i = 0;
    i = ++i + 1;
    

    在那条线之后, i 将是2。这种变化的原因是…因为它已经在实践中工作了,它要做的更多的工作是使它不被定义,而只是让它在C++ 11的规则中被定义(实际上,现在的工作比故意的改变更像是意外,所以 拜托 不要 用你的代码来做!).

    直接从马嘴里

    http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#637

        5
  •  9
  •   DigitalRoss    15 年前

    有两个选择:定义的还是未定义的,您会做什么选择?

    标准的作者有两个选择:定义行为或将其指定为未定义。

    考虑到最初编写此类代码显然是不明智的,为其指定结果毫无意义。人们会想阻止这样的代码,而不是鼓励它。它对任何事情都没有用处或必要。

    此外,标准委员会没有任何方法强迫编译器编写人员做任何事情。如果他们需要一个特定的行为,这个要求可能会被忽略。

    也有实际的原因,但我怀疑他们服从于上述的一般考虑。但是对于记录而言,这种表达式和相关类型所需的任何类型的行为都将限制编译器生成代码、分解公共子表达式、在寄存器和内存之间移动对象等的能力。C已经受到弱可见性限制的限制。像Fortran这样的语言早就意识到别名参数和全局参数是一个优化杀手,我相信它们只是禁止它们。

    我知道你对一个特定的表达式很感兴趣,但是任何给定构造的确切性质都不重要。预测一个复杂的代码生成器将要做什么并不容易,在愚蠢的情况下,语言试图不需要这些预测。

        6
  •  8
  •   Trent    15 年前

    本标准的重要部分是:

    它的存储值最多通过表达式的计算修改一次

    您可以修改值两次,一次使用+运算符,一次使用赋值

        7
  •  7
  •   mlvljr    15 年前

    请注意,您的标准副本已过时,仅在示例的第1行和第3行中包含一个已知(且已修复)错误,请参见:

    C++ Standard Core Language Issue Table of Contents, Revision 67, #351

    Andrew Koenig: Sequence point error: unspecified or undefined?

    这个主题不容易仅仅阅读标准(这个标准非常模糊:(在本例中)。

    例如,它是否定义良好(或未定义)、未指定或一般情况下,实际上不仅取决于语句结构,而且取决于执行时的内存内容(具体来说,变量值),另一个示例:

    ++i, ++i; //ok
    
    (++i, ++j) + (++i, ++j); //ub, see the first reference below (12.1 - 12.3)
    

    请看一看(全部清楚、准确):

    JTC1/SC22/WG14 N926 "Sequence Point Analysis"

    此外,安吉丽卡·兰格也有一篇关于这个主题的文章(尽管不像前一篇那么清楚):

    "Sequence Points and Expression Evaluation in C++"

    还有俄语的讨论(尽管评论和帖子本身有一些明显错误的陈述):

    "Точки следования (sequence points)"

        8
  •  4
  •   Kirill V. Lyadvinsky    15 年前

    以下代码演示了如何获得错误(意外)结果:

    int main()
    {
      int i = 0;
      __asm { // here standard conformant implementation of i = ++i + 1
        mov eax, i;
        inc eax;
        mov ecx, 1;
        add ecx, eax;
        mov i, ecx;
    
        mov i, eax; // delayed write
      };
      cout << i << endl;
    }
    

    结果将打印1。

        9
  •  4
  •   Daniel Daranas    14 年前

    假设你问“为什么语言是这样设计的?”.

    你说 i = ++i + i “明显未定义”,但 i = ++i + 1 应该离开 i 是否具有定义的值?坦率地说,这不太一致。我喜欢把每件事都定义得很好,或者总是不明确。在C++中,我有后者。从本质上来说,这不是一个非常糟糕的选择——一方面,它阻止了你编写邪恶的代码,在同一个“语句”中进行五到六次修改。

        10
  •  3
  •   int3    15 年前

    类比论证: 如果你把操作符看作是函数的类型,那么它就有点意义了。如果有一个类 operator= ,您的工作分配声明将等效于以下内容:

    operator=(i, ++i+1)
    

    (第一个参数实际上是通过 this 指针,但这只是为了说明。)

    对于纯函数调用,这显然是未定义的。第一个参数的值取决于第二个参数的计算时间。但是,对于基元类型,您可以忽略它,因为 i 只是被覆盖;它的值无关紧要。但是如果你在自己身上做其他的魔法 运算符= 然后差异就会显现出来。

    简而言之:所有的操作符都像函数一样,因此应该按照相同的概念进行操作。如果 i + ++i 未定义,则 i = ++i 也应该是未定义的。

        11
  •  2
  •   Ken Lange    15 年前

    怎么样,我们都同意永远不要,永远不要,写这样的代码?如果编译器不知道您想做什么,您如何期望后面的糟糕的SAP理解您想要做什么?把我放在自己的线上 杀了你。

        12
  •  1
  •   coppro    15 年前

    根本原因是编译器处理值读写的方式。编译器可以在内存中存储一个中间值,并且只在表达式的末尾提交该值。我们读了这个表达 ++i 作为“增加” i 然后返回它”,但编译器可能会将其视为“加载 ,添加一个,返回它,并在有人再次使用它之前将其提交回内存。建议编译器尽量避免读/写实际的内存位置,因为这样会减慢程序的运行速度。

    在特定情况下 i = ++i + 1 这主要是因为需要一致的行为规则。在这种情况下,许多编译器都会做“正确的事情”,但是如果 S实际上是一个指针,指向 ?如果没有这个规则,编译器必须非常小心,以确保以正确的顺序执行加载和存储。此规则用于允许更多优化机会。

    类似的情况是所谓的严格混叠规则。不能指定值(例如, int )通过一个不相关类型的值(例如, float )只有少数例外。这使得编译器不必担心 float * 使用将更改 int 大大提高了优化潜力。

        13
  •  1
  •   Billy ONeal IS4    15 年前

    这里的问题是,标准允许编译器在执行语句时完全重新排序。但是,不允许对语句重新排序(只要任何这样的重新排序都会导致程序行为发生变化)。因此,表达式 i = ++i + 1; 可通过两种方式进行评估:

    ++i; // i = 2
    i = i + 1;
    

    i = i + 1;  // i = 2
    ++i;
    

    i = i + 1;  ++i; //(Running in parallel using, say, an SSE instruction) i = 1
    

    当您在混合中抛出用户定义的类型时,情况会更糟,在这种情况下,++运算符可以对类型的作者想要的类型产生任何影响,在这种情况下,计算中使用的顺序非常重要。

        14
  •  1
  •   Prasoon Saurav    14 年前

    i=v[i++];//未指定行为
    i=++i+1;//未指定行为

    以上所有表达式都调用未定义的行为。

    i=7,i++,i++;//i变为9

    这很好。

    阅读Steve Summit的C-FAQ。

        15
  •  0
  •   Mister    15 年前

    ++i ,我必须分配“1”,但是 i = ++i + 1 ,必须为其指定值“2”。由于没有中间的序列点,编译器可以假定同一个变量不会被写入两次,因此这两个操作可以以任何顺序执行。所以是的,如果最终值是1,编译器将是正确的。