C++使用对正在定义的变量的引用

C++ Using a reference to the variable being defined

本文关键字:定义 变量 引用 C++      更新时间:2023-10-16

根据标准(扣除…s),以下代码是有效的C++吗?

bool f(T& r)
{
if(...)
{
r = ...;
return true;
}
return false;
}
T x = (f(x) ? x : T());

众所周知,在这个项目使用的GCC版本中进行编译(4.1.2和3.2.3……甚至不要让我开始……),但应该吗?

编辑:我添加了一些细节,例如关于f()在原始代码中的概念外观。基本上,它意味着在特定条件下初始化x

语法上是这样的,但是如果你尝试这个

#include <iostream>
using namespace std;
typedef int T;
bool f(T& x)
{
return true;
}
int main()
{
T x = (f(x) ? x : T());
cout << x;
}

它输出一些随机垃圾。但是,如果您修改

bool f(T& x)
{
x = 10;
return true;
}

则它输出10。在第一种情况下,对象x被声明,编译器分配一些伪任意值(因此您不初始化它),而在第二种情况中,您在声明后专门分配一个值(T(),即0),即初始化它

我认为你的问题与此类似:在初始化中使用新声明的变量(int x=x+1)?

它无疑应该编译,但可能会有条件地导致未定义的行为

  • 如果T是非基元类型,则未定义行为(如果已赋值)
  • 如果T是基元类型,则定义良好的行为(如果它是非局部的),如果在读取之前没有赋值则定义未定义的行为(字符类型除外,在字符类型中,它被定义为提供未指定的值)

标准的相关部分是3.8中的规则,对象寿命:

类型为T的对象的生存期从以下时间开始:

获得了T型具有适当排列和大小的
  • 存储,并且
  • 如果对象有非平凡的初始化,它的初始化就完成了

所以x的寿命还没有开始。在同一节中,我们找到了使用x:进行管理的规则

类似地,在对象的生存期开始之前,但在分配了对象将占用的存储之后,或者在对象的生命期结束之后,在重用或释放对象所占用的存储之前,任何引用原始对象的glvalue都可以使用,但只能以有限的方式使用。关于正在建造或毁坏的物体,请参见12.7。否则,这样的glvalue指的是已分配的存储(3.7.4.2),并且使用不依赖于其值的glvalue属性是定义良好的程序有未定义的行为,如果

  • 左值到右值的转换(4.1)应用于这样的glvalue,
  • glvalue用于访问非静态数据成员或调用对象的非静态成员函数,或者
  • glvalue绑定到对虚拟基类的引用(8.5.3),或者
  • glvalue用作dynamic_cast(5.2.7)的操作数或typeid的操作数

如果您的类型是非基元的,那么尝试分配它实际上是对非静态成员函数T::operator=的调用。完全停止,根据情况2,这是未定义的行为。

基元类型是在不调用成员函数的情况下分配的,所以现在让我们仔细看看第4.1节,从左值到右值的转换,看看左值到右值的转换何时会是未定义的行为:

当未赋值的操作数或其子表达式中发生左值到右值的转换时(第5条),不访问引用对象中包含的值。在所有其他情况下,转换的结果根据以下规则确定:

  • 如果T是(可能cv限定)std::nullptr_t,则结果为空指针常量(4.10)
  • 否则,如果T具有类类型,则转换副本从glvalue初始化T类型的临时,并且转换的结果是临时的prvalue
  • 否则,如果glvalue引用的对象包含无效的指针值(3.7.4.2、3.7.4.3),则行为是实现定义的
  • 否则,如果T是一个(可能是cv限定的)无符号字符类型(3.9.1),并且glvalue引用的对象包含一个不确定值(5.3.4、8.5、12.6.2),并且该对象没有自动存储持续时间,或者glvalue是一元&运算符的操作数,或者绑定到引用,则结果为未指定值
  • 否则,如果glvalue引用的对象包含不确定的值,则行为未定义
  • 否则,glvalue指示的对象中包含的值就是prvalue结果

(注意,这些规则反映了对即将到来的C++14标准的重写,以使其更容易理解,但我认为这里的行为没有实际变化)

您的变量x在进行左值引用并传递给f()时有一个不确定的值。只要该变量具有基元类型,并且其值在读取之前被赋值(读取是左值到右值的转换),代码就可以了。

如果变量在读取之前没有赋值,效果取决于T。字符类型将导致代码执行并使用任意但合法的字符值。所有其他类型都会导致未定义的行为。


1除非x具有静态存储持续时间,例如全局变量。在这种情况下,根据第3.6.2节非局部变量的初始化:,在执行前初始化为零

在进行任何其他初始化之前,具有静态存储持续时间(3.7.1)或线程存储持续时间的变量应为零初始化(8.5)。

在这种静态存储持续时间的情况下,不可能运行未指定值的左值到右值转换。但是零初始化并不是所有类型的有效状态,所以仍然要小心。

虽然作用域起着一定的作用,但真正的问题是对象的生存期,更确切地说,对于具有非平凡初始化的对象,生存期何时开始。

这与初始化表达式是否可以使用变量本身密切相关?以及将C++对象传递到它自己的构造函数中是否合法?。虽然我对这些问题的回答并没有很好地回答这个问题,所以它看起来不像是重复的。

我们在这里关注的C++标准草案的关键部分是3.8[basic.life]部分,其中写道:

对象的生存期是该对象的运行时属性一个对象被认为具有非平凡的初始化如果它是一个类或聚合类型,并且它或它的一个成员是由非平凡的构造函数初始化的默认构造函数。[注意:由琐碎的复制/移动构造函数进行的初始化是非琐碎的初始化--结束语]类型T的对象的生存期从:开始

获得了T型具有适当排列和大小的
  • 存储,并且
  • 如果对象具有非平凡的初始化,那么它的初始化就完成了

因此,在这种情况下,我们满足第一个子弹,存储已经获得。

第二颗子弹是我们发现麻烦的地方:

  • 我们有非平凡的初始化吗
  • 如果是,初始化是否完成

非平凡初始化情况

我们可以从缺陷报告363中得到一个基本推理,该报告要求:

如果是,UDT的自初始化的语义是什么?例如

#include <stdio.h>
struct A {
A()           { printf("A::A() %pn",            this);     }
A(const A& a) { printf("A::A(const A&) %p %pn", this, &a); }
~A()          { printf("A::~A() %pn",           this);     }
};
int main()
{
A a=a;
}

可以编译和打印:

A::A(const A&) 0253FDD8 0253FDD8
A::~A() 0253FDD8

,并且所提出的分辨率为:

3.8[basic.life]第6段表明此处的引用是有效的。允许将类对象的地址放在它之前已完全初始化,并且允许将其作为参数传递给引用参数,只要引用可以直接绑定即可。[…]

因此,在对象的生存期开始之前,我们对对象所能做的事情是有限的。我们可以从缺陷报告中看到,绑定对x的引用是有效的,只要它直接绑定即可。

我们能做的在第3.8节(缺陷报告引用的同一节和段落)中介绍(emphasis mine

类似地,在对象的生存期开始之前对象将占用的存储已被分配,或者在对象的生存期已结束,并且在存储之前被占用的对象被重用或释放,任何引用可以使用原始对象,但只能以有限的方式使用。对于对象正在建造或毁坏,见12.7。否则,这样的glvalue指已分配的存储(3.7.4.2),并使用不依赖于其值的glvalue是定义良好的程序具有未定义的行为,如果:

  • 左值到右值的转换(4.1)应用于这样的glvalue,

  • glvalue用于访问的非静态数据成员或调用对象,或

  • glvalue绑定到对虚拟基类(8.5.3)或的引用

  • glvalue用作dynamic_cast(5.2.7)的操作数或typeid的操作数。

在您的情况下,我们在此处访问非静态数据成员,请参阅上面的重点:

r = ...;

因此,如果T具有非平凡的初始化,那么这一行调用未定义的行为,从r读取也将是一个访问,在缺陷报告1531中涵盖。

如果x具有静态存储持续时间,它将被初始化为零,但据我所知,这不算作,它的初始化已完成,因为构造函数将在动态初始化期间调用。

琐碎初始化案例

如果T具有琐碎的初始化,那么一旦获得存储,则寿命就开始了,并且写入r是定义良好的行为。尽管要注意,在r初始化之前读取它将调用未定义的行为,因为它将产生一个不确定的值。如果x有静态存储持续时间,那么它是零初始化的,我们没有这个问题。

如果它编译,无论在哪种情况下,无论您调用的是未定义的行为还是不允许编译。编译器没有义务为未定义的行为生成诊断,尽管可能。它只义务为格式错误的代码生成诊断,这里没有任何麻烦的情况。