初始化相互引用的对象

时间:2012-01-03 23:07:34

标签: c++ language-lawyer

考虑以下一对相互引用类型:

struct A;
struct B { A& a; };
struct A { B& b; };

这可以通过GCC,Clang,Intel,MSVC中的聚合初始化来初始化,但不能通过SunPro来初始化,因为SunPro坚持要求用户定义的ctors。

struct {A first; B second;} pair = {pair.second, pair.first};

这种初始化是否合法?

稍微详细的演示:http://ideone.com/P4XFw

现在,听从Sun的警告,用户定义的构造函数的类怎么样?以下适用于GCC,clang,Intel,SunPro和MSVC,但它是否合法?

struct A;
struct B { A& ref; B(A& a) : ref(a) {} };
struct A { B& ref; A(B& b) : ref(b) {} };

struct {B first; A second;} pair = {pair.second, pair.first};

演示:http://ideone.com/QQEpA

最后,如果容器也不是微不足道的,例如, (适用于G ++,Intel,Clang(带警告),但不适用于MSVC(初始化程序中未知的“配对”)或SunPro(“配对不是结构”)

std::pair<A, B> pair(pair.second, pair.first);

从我所看到的情况来看,§3.8[basic.life]/6禁止在生命周期开始之前访问非静态数据成员,但是对于第二次“对”第二次“访问”的左值评估是什么?如果是,那么这三个初始化都是非法的吗?此外,§8.3.2[dcl.ref]/5说“引用应该被初始化以引用一个有效的对象”,这可能使所有三个都是非法的,但也许我遗漏了一些东西,编译器也接受了这个。

PS:我意识到这些课程在任何方面都不实用,因此语言律师标签。这里有相关且稍微更实际的旧讨论:Circular reference in C++ without pointers

2 个答案:

答案 0 :(得分:1)

这个一开始就让我心烦意乱,但我想我现在已经明白了。根据1998标准的12.6.2.5,C ++保证数据成员按照它们在类中声明的顺序进行初始化,并且构造函数体在所有成员初始化之后执行。这意味着表达式

struct A;
struct B { A& a; };
struct A { B& b; };
struct {A first; B second;} pair = {pair.second, pair.first};

有意义,因为pair是一个auto(本地,堆栈)变量,因此编译器知道它的相对地址和成员地址,并且没有第一个和第二个构造函数。

为什么这两个条件意味着上面的代码是有意义的:first类型A的构造(pair的任何其他数据成员之前),first数据成员b设置为引用pair.second,其地址为编译器所知,因为它是一个堆栈变量(程序中已存在空间,AFAIU)。请注意,pair.second作为对象,即内存段,尚未初始化(包含垃圾),但这并未改变在编译时已知该垃圾的地址这一事实并可用于设置引用。由于A没有构造函数,因此它无法尝试对b执行任何操作,因此行为已明确定义。初始化first后,轮到second,并且相同:其数据成员a引用pair.first,其类型为A,并且pair.first地址由编译器知道。

如果编译器不知道地址(比如因为通过new运算符使用堆内存),则应该存在编译错误,如果没有,则应该是未定义的行为。虽然明智地使用放置新操作符可能允许它工作,但从那时起firstsecond的地址可以在first初始化时再次知道。

现在为变化:

struct A;
struct B { A& ref; B(A& a) : ref(a) {} };
struct A { B& ref; A(B& b) : ref(b) {} };
struct {B first; A second;} pair = {pair.second, pair.first};

与第一个代码示例的唯一区别是B构造函数是显式定义的,但汇编代码肯定是相同的,因为构造函数体中没有代码。因此,如果第一个代码示例有效,则第二个代码示

但是,如果代码在B的构造函数体中,它正在获取尚未初始化的东西(pair.second)的引用(但是对于哪个地址是定义和已知),并且该代码使用a,很明显你正在寻找麻烦。如果你很幸运,你会遇到崩溃,但写入a可能会无声地失败,因为当最终调用A构造函数时,值会被覆盖。的

答案 1 :(得分:1)

从编译器的角度来看,引用只不过是const指针。用指针重写你的例子,它变得清晰如何以及为什么它起作用:

struct A;
struct B { A* a; };
struct A { B* b; };
struct {A first; B second;} pair = {&(pair.second), &(pair.first)}; //parentheses for clarity

Schollii写道:记忆是事先分配的,因此是可以解决的。由于引用/指针,没有访问权限或评估权。那只是采用“第二”和“第一”的简单指针算法的地址。

我可以大肆宣扬在运营商以外的任何地方使用引用是如何滥用语言,但我认为这个例子很好地突出了这个问题:)

(从现在开始,我手动编写所有ctors。您的编译器可能会也可能不会自动执行此操作。) 尝试使用新的:

struct A;
struct B { A& a; B(A& arg):a(arg){;} };
struct A { B& b; A(B& arg):b(arg){;} };
typedef struct PAIR{A first; B second; PAIR(B& argB, A& argA):first(argB),second(argA){;}} *PPAIR, *const CPPAIR;
PPAIR pPair = NULL;// just to clean garbage or 0xCDCD
pPair = new PAIR(pPair->second, pPair->first);

现在它取决于执行顺序。如果最后进行分配(在ctor之后),则second.p将指向0x0000并且first.ref将指向例如。 0x0004单元。
实际上,http://codepad.org/yp911ug6这里是最后运行的ctors(最有意义!),因此一切正常(即使看起来不应该)。

不能谈论模板。

但你的问题是“那是合法的吗?”。没法律禁止它。
它会起作用吗?好吧,我不相信编译器制造商足以就此做出任何陈述。