C ++使用对正在定义的变量的引用
根据标准,以下代码是否为有效的C ++(折扣... s)?
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
的对象的生命周期从以下时间开始:
所以x
的寿命还没有开始。 在同一节中,我们找到了使用x
的规则:
类似地, 在对象的生命周期开始之前,但是在对象将占用的存储之后,或者在对象的生命周期结束之后并且在重新使用或释放该对象占用的存储之前, 任何引用的对象的glvalue可以使用原始对象,但仅限于有限的方式 。 正在建造或破坏的物体见12.7。 否则, 这样的glvalue指的是分配的存储(3.7.4.2),并且使用不依赖于它的值的glvalue的属性是明确的。 如果出现以下情况,程序具有未定义行
如果你的类型是非原始的,那么试图分配它实际上是对T::operator=
一个非静态成员函数的调用。 根据情况2,这是未定义的行为。
原始类型是在不调用成员函数的情况下分配的,因此现在让我们仔细看看4.1节左值到右值的转换,以确定左值到右值转换的确切时间是未定义行为:
当在未评估操作数或其子表达式中出现左值到右值转换(第5章)时,不会访问被引用对象中包含的值。 在所有其他情况下,转换结果根据以下规则确定:
T
(可能是cv-qualified) std::nullptr_t
,结果是一个空指针常量(4.10)。 T
有一个类类型,则转换copy-从glvalue初始化类型T
的临时值,并且转换的结果是临时值的前值。 T
是一个(可能是cv限定的)无符号字符类型(3.9.1),并且glvalue引用的对象包含一个不确定的值(5.3.4,8.5,12.6.2),并且该对象不有自动存储持续时间,或者glvalue是一元&
运算符的操作数,或者它被绑定到一个引用,结果是一个未指定的值。 (注意,这些规则反映了为即将到来的C ++ 14标准重写以便更容易理解,但我认为这里没有实际的行为变化)
您的变量x
在创建左值引用并将其传递给f()
时具有不确定的值。 只要该变量具有原始类型,并且在读取它之前分配了它的值(读取是左值到右值的转换),代码就很好。
如果变量在读取之前未被分配,则效果取决于T
字符类型将导致代码执行并使用任意但合法的字符值。 所有其他类型导致未定义的行为。
1除非x
具有静态存储持续时间,例如全局变量。 在这种情况下,根据3.6.2非局部变量的初始化,它在执行之前是零初始化的:
静态存储持续时间(3.7.1)或线程存储持续时间(3.7.2)的变量在进行任何其他初始化之前应进行零初始化(8.5)。
在这种静态存储持续时间的情况下,不可能运行未指定值的左值到右值转换。 但零初始化对于所有类型都不是有效的状态,所以仍然要小心。
虽然范围起着作用,但真正的问题是关于对象的生命周期,更确切地说,对于具有非平凡初始化的对象,生命周期何时开始。
这与可以初始化表达式使用变量本身密切相关? 并传递一个C ++对象到它自己的构造函数合法吗? 虽然我对这些问题的回答并不能很好地回答这个问题,但它看起来并不重复。
我们关心的C ++标准草案的关键部分是3.8
节[basic.life],它说:
对象的生命周期是对象的运行时属性。 如果一个对象是一个类或聚合类型,并且它的一个成员是由一个不重要的默认构造函数构造函数初始化的,那么它就被认为具有不平凡的初始化 。 [注意:通过简单的复制/移动构造函数进行初始化不是简单的初始化。 - 结束注释] 类型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
节(缺陷报告引用的相同部分和段落)中说明(重点是我的):
类似地,在对象的生命周期开始之前,但是在对象将占用的存储之后,或者在对象的生命周期结束之后并且在重新使用或释放该对象占用的存储之前,任何引用的对象的glvalue可以使用原始对象,但仅限于有限的方式。 正在建造或破坏的物体见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
有静态存储持续时间,那么它是零初始化的,我们没有这个问题。
如果要编译,无论是否调用未定义的行为,或者不允许编译。 虽然可能,但编译器没有义务为未定义的行为生成诊断。 只有对不合格的代码进行诊断才是有效的,这些代码都不是麻烦的情况。
链接地址: http://www.djcxy.com/p/79291.html