Bluehorn 的回答是正确的,但对我来说,它并没有用最简单的术语解释问题的原因。我的理解方式如下:
如果 NonPOD 是非 POD 类,那么当你这样做时:
NonPOD np;
np.field;
编译器不一定通过向基指针添加一些偏移量和取消引用来访问该字段。对于 POD 类,C++ 标准限制它这样做(或等效的东西),但对于非 POD 类,它没有。编译器可能会改为从对象中读取指针,向该值添加偏移量以给出字段的存储位置,然后取消引用。如果字段是 NonPOD 的虚拟基的成员,则这是虚拟继承的常见机制。但不限于这种情况。编译器几乎可以做任何它喜欢的事情。如果需要,它可以调用隐藏的编译器生成的虚拟成员函数。
在复杂的情况下,显然不可能将字段的位置表示为整数偏移量。所以offsetof
在非 POD 类上无效。
如果您的编译器恰好以简单的方式存储对象(例如单继承,通常甚至是非虚拟多继承,并且通常在您引用对象的类中定义的字段,而不是在某些基类中),那么它就会碰巧起作用。可能有些情况恰好适用于每个单独的编译器。这并不能使它有效。
附录:虚拟继承是如何工作的?
对于简单的继承,如果 B 是从 A 派生的,通常的实现是指向 B 的指针只是指向 A 的指针,B 的附加数据卡在末尾:
A* ---> field of A <--- B*
field of A
field of B
使用简单的多重继承,您通常假设 B 的基类(称为 'em A1 和 A2)以 B 特有的某种顺序排列。但是指针的相同技巧不起作用:
A1* ---> field of A1
field of A1
A2* ---> field of A2
field of A2
A1 和 A2 对它们都是 B 的基类这一事实“一无所知”。因此,如果您将 B* 转换为 A1*,它必须指向 A1 的字段,如果您将其转换为 A2* 它必须指向 A2 的字段。指针转换运算符应用偏移量。所以你可能会得到这个:
A1* ---> field of A1 <---- B*
field of A1
A2* ---> field of A2
field of A2
field of B
field of B
然后将 B* 转换为 A1* 不会更改指针值,但将其转换为 A2* 会增加sizeof(A1)
字节。这就是为什么在没有虚拟析构函数的情况下,通过指向 A2 的指针删除 B 出错的“其他”原因。它不仅没有调用 B 和 A1 的析构函数,甚至没有释放正确的地址。
无论如何,B“知道”它的所有基类在哪里,它们总是存储在相同的偏移量处。所以在这种安排下,offsetof 仍然可以工作。该标准不要求实现以这种方式进行多重继承,但他们经常这样做(或类似的事情)。因此,在这种情况下,offsetof 可能对您的实现起作用,但不能保证。
现在,虚拟继承呢?假设 B1 和 B2 都以 A 作为虚拟基础。这使它们成为单继承类,因此您可能认为第一个技巧将再次起作用:
A* ---> field of A <--- B1* A* ---> field of A <--- B2*
field of A field of A
field of B1 field of B2
但坚持住。当 C 从 B1 和 B2 派生(为了简单起见,非虚拟)时会发生什么?C 只能包含 A 的字段的 1 个副本。这些字段不能紧接在 B1 的字段之前,也不能紧接在 B2 的字段之前。我们有麻烦了。
所以实现可能会做的是:
// an instance of B1 looks like this, and B2 similar
A* ---> field of A
field of A
B1* ---> pointer to A
field of B1
虽然我已经指出 B1* 指向 A 子对象之后的对象的第一部分,但我怀疑(不费心检查)实际地址不会在那里,它将是 A 的开始。只是不像简单的继承,指针中的实际地址和我在图中指出的地址之间的偏移量,除非编译器确定对象的动态类型,否则永远不会使用。相反,它将始终通过元信息正确到达 A。所以我的图表将指向那里,因为该偏移量将始终应用于我们感兴趣的用途。
指向 A 的“指针”可以是指针或偏移量,这并不重要。在 B1 的实例中,创建为 B1,它指向(char*)this - sizeof(A)
,并且在 B2 的实例中相同。但是如果我们创建一个 C,它可能看起来像这样:
A* ---> field of A
field of A
B1* ---> pointer to A // points to (char*)(this) - sizeof(A) as before
field of B1
B2* ---> pointer to A // points to (char*)(this) - sizeof(A) - sizeof(B1)
field of B2
C* ----> pointer to A // points to (char*)(this) - sizeof(A) - sizeof(B1) - sizeof(B2)
field of C
field of C
因此,使用指针或对 B2 的引用访问 A 的字段需要的不仅仅是应用偏移量。我们必须读取 B2 的“指向 A 的指针”字段,跟随它,然后才应用偏移量,因为根据 B2 的基类,该指针将具有不同的值。没有这样的事情offsetof(B2,field of A)
:不可能。在任何实现上, offsetof永远不会与虚拟继承一起使用。