初始化相互引用的对象

19
考虑下面这组互相引用的类型:
struct A;
struct B { A& a; };
struct A { B& b; };

在GCC、Clang、Intel和MSVC中,可以使用聚合初始化来进行初始化,但SunPro坚持要求必须使用用户定义的构造函数。

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(“pair”未知于初始化程序)或SunPro(“pair不是一个结构”)中不起作用)

std::pair<A, B> pair(pair.second, pair.first);
据我所见,§3.8[basic.life]/6禁止在对象生命周期开始之前访问非静态数据成员,但是对pair.second的lvalue求值是否算作对second的"访问"呢?如果是的话,那么这三个初始化是否都是非法的呢?另外,§8.3.2[dcl.ref]/5说:“参考必须被初始化为指向有效对象”,这可能也使这三个初始化都非法了,但也许我漏看了什么,编译器出于某种原因接受这种情况。 PS:我意识到这些类在任何情况下都不实用,因此使用了“语言律师”标签。相关且稍微更实用的旧讨论在这里:Circular reference in C++ without pointers

2
我的直觉是你的聚合构造是正确的(虽然我现在无法证明),而使用 std::pair 的非聚合版本则肯定不允许,原因就像你所说的那样。你可以问一个更简单的问题:struct Foo { Foo & r; Foo(Foo & f) : r(f) { } } x(x); - Kerrek SB
你能用指针让它工作吗? - Karl Knechtel
据我所知,第二个示例是不合法的,因为只有聚合体(可能没有用户声明的构造函数)才能有花括号初始化器:§8.5[dcl.init]/14 - Emil Styrke
相关:在基类构造函数完成之前传递this:UB还是危险?(我相信,它几乎回答了你的问题,“这是不可能的”) - bitmask
2个回答

1

从编译器的角度来看,引用不过是常量指针。使用指针重写你的例子,就能清楚地看到它是如何工作的以及为什么会这样。

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

正如Schollii所写:内存是预先分配的,因此可寻址。由于引用/指针,没有访问或评估。这仅仅是获取“second”和“first”的地址,简单的指针算术。

我可以抱怨在除了运算符之外的任何地方使用引用都是语言滥用,但我认为这个例子足以突出问题 :)

(从现在开始,我手动编写所有构造函数。您的编译器可能会自动执行此操作。)尝试使用new:

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);

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

但是关于模板,我无话可说。

但是你的问题是“是否合法”。没有法律禁止它。
它会工作吗?嗯,我不太信任编译器制造商对此做出任何声明。


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是一个自动(本地、堆栈)变量,所以它的相对地址和成员的地址对编译器来说是已知的,并且first和second没有构造函数。

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

如果编译器不知道地址(例如使用 new 运算符通过堆内存),则应该会出现编译错误,或者如果没有,则是未定义的行为。尽管谨慎地使用放置 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将很可能无声地失败,因为这些值稍后会被覆盖。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接