【问题标题】:Is modifying a variable in its declaration statement well-defined?在其声明语句中修改变量是否定义明确?
【发布时间】:2018-04-19 02:41:06
【问题描述】:

例如:

#include<iostream>
using namespace std;
int main() {
    int i = i=0; //no warning
    cout << i << endl;
    return 0;
}

在 vs2015 中编译,没有警告并输出 0。虽然看起来有点奇怪,但这段代码 sn-p 是否定义明确?

但是,在这个 online compiler (g++ prog.cc -Wall -Wextra -std=c++17) 中,它会引发以下警告:

prog.cc: In function '`int main()`':  
prog.cc:8:12: warning: operation on '`i`' may be undefined [-Wsequence-point]
     `int i=i=0;`

【问题讨论】:

  • 请注意,此处的“修改”一词与“分配”不是同义词。我很确定像int i = i += 5 这样的东西not 是明确定义的。不过我对此一无所知。
  • @PatrickRoberts int i = i&amp;0; 怎么样?这里“赋值”可能更好,但是“修改”可以。因为你说的是​​在它的未定义值的基础上修改,这是修改的另一种情况。
  • 我很确定我知道答案,但是除了无聊的好奇心之外,你还有什么理由需要知道这个吗?
  • 我担心标准可能允许i=0 运行,然后i 被初始化为未指定的状态,然后ii 初始化。我还担心在第一次初始化之前对i 的写入/读取可能是未定义的,而不仅仅是未指定的值。你使用int 的事实让我不太确定。
  • @PatrickRoberts 不仅对结果感到好奇,我很困惑为什么在 c++ 中允许这样的语句,如 int i=i=0 而不是 int i=0。似乎 i 在它之前使用定义。

标签: c++ language-lawyer undefined-behavior


【解决方案1】:

有两种可能的情况,取决于对象的生命周期是否已经开始。这是由[basic.life]中的第一条规则决定的:

对象或引用的生命周期是对象或引用的运行时属性。如果一个对象是类或聚合类型,并且它或其子对象之一由 构造函数而不是普通的默认构造函数。 [注意:通过简单的复制/移动构造函数进行初始化是非空初始化。 — 尾注] T 类型对象的生命周期开始于:

  • 获得了与 T 类型正确对齐和大小的存储,并且
  • 如果对象具有非空初始化,则其初始化完成,除非该对象是联合成员或其子对象,其生命周期仅在该联合成员是联合中的已初始化成员时才开始,或如中所述([class.union])。
  1. 类或聚合类型的对象

    std::string s = std::to_string(s.size()); // UB
    

    在这种情况下,对象的生命周期直到初始化完成才开始,因此[basic.life]中的这条规则适用:

    类似地,在对象的生命周期开始之前但在对象将占用的存储空间已分配之后,或者在对象的生命周期结束之后并且在对象占用的存储空间被重用或释放之前,任何glvalue可以使用指代原始对象的那个,但只能以有限的方式使用。有关正在构建或销毁的对象,请参阅 ([class.cdtor])。否则,这样的glvalue指的是分配的存储,并且使用不依赖于其值的glvalue的属性是明确定义的。这 程序在以下情况下具有未定义的行为:

    • glvalue 用于访问对象,或
    • glvalue 用于调用对象的非静态成员函数,或
    • glvalue 绑定到对虚拟基类的引用,或者
    • glvalue 用作dynamic_cast 的操作数或typeid 的操作数。

    在本例中,glvalue 用于访问非静态成员,导致未定义的行为。

  2. 原始类型的对象

    int i = (i=0); // ok
    int k = (k&0); // UB
    

    这里,即使有初始化器,由于类型的原因,初始化也不能是非空的。因此,对象的生命周期已经开始,上述规则不适用。

    仍然,对象中的现有值是不确定的(除非对象具有静态存储持续时间,在这种情况下静态初始化将其值设为零)。引用具有不确定值的对象的左值绝对不能进行左值到右值的转换。因此“只写”操作是允许的,但大多数1 操作读取不确定值会导致未定义的行为。

    适用规则见[dcl.init]

    如果没有为对象指定初始化器,则该对象是默认初始化的。 当对象获得自动或动态存储时长的存储时,该对象有一个不确定的值,如果没有对该对象执行初始化,该对象将保留一个不确定的值,直到该值被替换。 [注意:具有静态或线程存储持续时间的对象是零初始化的,请参阅([basic.start.static])。 ——尾注]

    如果评估产生不确定的值,则行为未定义,但以下情况除外

    • 如果无符号窄字符类型或std::byte 类型的不确定值是 由评估产生:

      • 条件表达式的第二个或第三个操作数,
      • 逗号表达式的右操作数,
      • 强制转换或转换为无符号窄字符类型或std::byte 类型的操作数,或
      • 丢弃值表达式,

      那么运算的结果就是一个不确定的值。

    • 如果一个不确定的无符号窄字符类型或std::byte类型的值是由一个简单赋值运算符的右操作数的求值产生的,该运算符的第一个操作数是一个无符号窄字符类型或std::byte类型的左值,一个不确定值替换左操作数引用的对象的值。
    • 如果在初始化无符号窄字符类型的对象时初始化表达式的评估产生了无符号窄字符类型的不确定值,则该对象将被初始化为不确定值。
    • 如果在初始化std::byte 类型的对象时通过初始化表达式的评估产生了无符号窄字符类型或std::byte 类型的不确定值,则该对象被初始化为不确定值。

1 使用字符类型复制不确定的值有一个狭隘的例外,使目标值也不确定。该值仍然不能用于其他操作,例如按位运算符或算术。

【讨论】:

  • 那么评估订单问题呢?如果初始化器的副作用没有在初始化之前排序,那么行为仍然是未定义的。
  • @xskxzr:您指的是最终值不同的情况,例如int x = (x = 1) - 1;?我会调查那个。
  • 即使最终的值相同,也可能是未定义的。见here
  • @xskxzr:如果标准真的给出了副作用的正式定义不是很好吗?
  • Here 是定义(尽管可能是非正式的)。
【解决方案2】:

在其声明语句中修改变量是否定义明确?

int i = i=0;//no warning

上面的语句是initialization,并且定义明确,因为两个i在同一个范围内。

根据basic.scope.pdecl#1

名称的声明点在其完成后立即 声明器和它的初始化器(如果有的话)之前,除非如下所述。 [ 示例:

unsigned char x = 12; // Warning -Wunused-variable
{ unsigned char x = x; }
                ^ warning -Wuninitialized

这里第二个 x 用它自己的(不确定的)值初始化。 — 结束示例 ]

在示例中,第二个x 位于不同的块作用域中,因此它的值是不确定的。并且会有警告:

warning: 'x' is used uninitialized in this function [-Wuninitialized]

鉴于具有自动存储功能的局部变量如果未初始化将具有不确定的值,我相信有一个具有此顺序的赋值发生。

int (i = (i = 0));

示例

int x; // indeterminate
int i = i = x = 2; // x is assigned to two, then x's value is assigned to i
cout << x << " " << i; // i = 2, x = 2

【讨论】:

  • 这不是 OP 所要求的
  • @PasserBy,我基于标题和 OP 的最后评论“我很困惑为什么在 c++ 中允许这样的声明,如 int i=i=0 而不是 int i=0。看起来i 在其定义之前使用。"
  • @PasserBy,如果您认为它完全超出范围,请随时帮助我改进我的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-16
  • 2023-03-20
  • 1970-01-01
相关资源
最近更新 更多