为什么不能在C ++的非POD结构上使用offsetof?
我正在研究如何在C ++中获得成员的内存偏移量,并在wikipedia中遇到这个问题:
在C ++代码中,您不能使用offsetof来访问不是普通旧数据结构的结构或类的成员。
我试了一下,似乎工作正常。
class Foo { private: int z; int func() {cout << "this is just filler" << endl; return 0;} public: int x; int y; Foo* f; bool returnTrue() { return false; } }; int main() { cout << offsetof(Foo, x) << " " << offsetof(Foo, y) << " " << offsetof(Foo, f); return 0; }
我收到了一些警告,但它编译和运行时给出了合理的输出:
Laptop:test alex$ ./test 4 8 12
我觉得我要么误解了POD的数据结构,要么我错过了其他一些难题。 我不明白是什么问题。
简短的回答:offsetof是一个function,只有在C ++标准的传统C兼容性。 因此它基本上被限制在比在C中可以完成的东西。C ++只支持它必须为C兼容性。
由于offsetof基本上是一种依赖于支持C的简单内存模型的hack(被实现为macros),因此如何组织C ++编译器实现器来组织类实例布局将会带来很大的自由。
其效果是,在C ++中,即使没有标准支持,offsetof也会经常工作(取决于所使用的源代码和编译器) – 除非没有标准支持。 所以你应该非常小心在C ++中使用offsetof,特别是因为我不知道一个编译器会产生非POD使用的警告。
编辑 :正如你所要求的例子,以下可能会澄清这个问题:
#include <iostream> using namespace std; struct A { int a; }; struct B : public virtual A { int b; }; struct C : public virtual A { int c; }; struct D : public B, public C { int d; }; #define offset_d(i,f) (long(&(i)->f) - long(i)) #define offset_s(t,f) offset_d((t*)1000, f) #define dyn(inst,field) {\ cout << "Dynamic offset of " #field " in " #inst ": "; \ cout << offset_d(&i##inst, field) << endl; } #define stat(type,field) {\ cout << "Static offset of " #field " in " #type ": "; \ cout.flush(); \ cout << offset_s(type, field) << endl; } int main() { A iA; B iB; C iC; D iD; dyn(A, a); dyn(B, a); dyn(C, a); dyn(D, a); stat(A, a); stat(B, a); stat(C, a); stat(D, a); return 0; }
当试图定位a
内部typesB
的字段时,这将会崩溃,而当一个实例可用时它将工作。 这是因为虚拟inheritance,基类的位置存储在查找表中。
虽然这是一个人为的例子,但实现可以使用查找表来查找类实例的公共,受保护和私有部分。 或者使查找完全dynamic(对于字段使用散列表)等。
标准只是通过限制POF的偏移量来打开所有的可能性(IOW:没有办法为POD结构使用哈希表… 🙂
只是另一个说明:我不得不在这个例子中重新实现offsetof(这里:offset_s),因为当我调用一个虚拟基类的字段的offsetof时,GCC实际上会出错。
Bluehorn的回答是正确的,但对我而言,并不能以最简单的方式解释问题的原因。 我理解的方式如下:
如果NonPOD是一个非POD类,那么当你这样做时:
NonPOD np; np.field;
编译器不一定通过向基指针添加一些偏移量并取消引用来访问该字段。 对于一个POD类,C ++标准限制它做这个(或者其他类似的东西),但是对于一个非POD类来说,它没有。 编译器可能会读取指向对象的指针, 向该值添加一个偏移量以指定该字段的存储位置,然后解除引用。 如果该字段是NonPOD的虚拟基础的成员,则这是具有虚拟inheritance的常见机制。 但不限于这种情况。 编译器可以做任何喜欢的事情。 如果需要的话,它可以调用隐藏的编译器生成的虚拟成员函数。
在复杂情况下,显然不可能将字段的位置表示为整数偏移量。 所以offsetof
在非POD类中是无效的。
如果您的编译器恰好以简单的方式存储对象(例如单一inheritance,通常甚至是非虚拟多重inheritance,并且通常在引用对象的类中正确定义了字段,而不是在一些基类中),那么它就会发生作用。 可能有这样的情况发生在每个单独的编译器上。 这不是有效的。
附录:虚拟inheritance如何工作?
通过简单的inheritance,如果B是从A派生的,通常的实现是指向B的指针只是指向A的指针,B的附加数据在最后被阻塞:
A* ---> field of A <--- B* field of A field of B
对于简单的多重inheritance,通常假设B的基类(call'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“知道”它的所有基类在哪里,它们总是存储在相同的偏移处。 所以在这种安排抵消将仍然工作。 这个标准并不要求这样做的多重inheritance,但他们经常这样做(或类似的东西)。 所以在这种情况下,offsetof可能会在您的实现中起作用,但不能保证。
那么,虚拟inheritance呢? 假设B1和B2都有A作为虚拟基础。 这使得它们成为单inheritance类,所以你可能会认为第一个技巧会再次起作用:
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只能包含1个A字段的副本。这些字段不能立即在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的开始。简单的inheritance,指针中的实际地址和我在图中指出的地址之间的偏移将永远不会被使用,除非编译器确定了对象的dynamictypes。 相反,它将始终通过元信息正确地到达A. 所以我的图表将指向那里,因为这个偏移总是适用于我们感兴趣的用途。
对A的“指针”可能是一个指针或偏移量,这并不重要。 在创build为B1的B1实例中,它指向(char*)this - sizeof(A)
,在B2的实例中也是一样的。 但是如果我们创build一个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
因此,使用指针或引用来访问A的字段不仅仅需要应用偏移量。 我们必须读取B2的“指向A的指针”字段,按照它,然后才应用一个偏移量,因为取决于什么类B2的基础,该指针将有不同的值。 没有offsetof(B2,field of A)
东西offsetof(B2,field of A)
:不可能有。 在任何实现上,offsetof将永远不能使用虚拟inheritance。
一般来说,当你问“ 为什么不确定 ”时,答案是“ 因为标准是这样说的 ”。 通常情况下,理性有以下一个或多个原因:
-
在哪种情况下,很难静态检测。
-
angular落案件难以界定,没有人为界定特殊案件而痛苦;
-
它的用途大多被其他function所覆盖;
-
标准化时现有的做法各不相同,根据现有的实施scheme和scheme,被认为更加危害标准化。
回到抵消,第二个原因可能是占主导地位的。 如果你看看标准之前使用POD的C ++ 0X,现在使用“标准布局”,“布局兼容”,“POD”,允许更精确的情况。 而且现在抵消了需要“标准布局”的class级,这是委员会不想强制布局的情况。
您还必须考虑offsetof()的常见用法,即在指向对象的void *指针时获取字段的值。 多重inheritance – 虚拟与否 – 在使用上存在问题。
对于POD数据结构的定义,在这里你可以解释[已经在Stack Overflow的另一篇文章中发表]
C ++中的PODtypes是什么?
现在,来到你的代码,它正常工作正常。 这是因为,你正在尝试为你的类的公共成员(这是有效的)findoffsetof()。
请让我知道,正确的问题,如果我上面的观点,没有说明你的疑问。
我认为你的类适合POD的c ++ 0x定义。 g ++已经在其最新版本中实现了一些c ++ 0x。 我认为VS2008也有一些C ++ 0x位。
从维基百科的c ++ 0x文章
C ++ 0x将放宽关于POD定义的几条规则。
如果一个类/结构是微不足道的,标准布局,并且它的所有非静态成员都是POD,那么它就被认为是一个POD。
一个普通的类或结构被定义为:
- 有一个微不足道的默认构造函数。 这可以使用默认的构造函数语法(SomeConstructor()= default;)。
- 有一个简单的复制构造函数,它可以使用默认的语法。
- 有一个简单的复制分配操作符,它可以使用默认的语法。
- 有一个微不足道的析构函数,它不能是虚拟的。
标准布局类或结构被定义为:
- 只有标准布局types的非静态数据成员
- 对所有非静态成员具有相同的访问控制(public,private,protected)
- 没有虚拟function
- 没有虚拟基类
- 只有基类是标准布局types的
- 没有与第一个定义的非静态成员相同types的基类
- 要么没有包含非静态成员的基类,要么在派生类最多的时候没有非静态数据成员,而最多只有一个包含非静态成员的基类。 实质上,这个类的层次结构中可能只有一个具有非静态成员的类。
你还有点POD。 尝试添加可能被覆盖的构造函数和公共方法。
你的对象将需要exta(隐藏)的数据来pipe理虚拟调度,因此它比看起来大,因此抵消有麻烦,
例如,如果添加一个虚拟的空析构函数:
virtual ~Foo() {}
你的类将变成“多态”,即它将有一个隐藏的成员字段,这是一个指向包含指向虚拟函数的“vtable”的指针。
由于隐藏的成员字段,对象的大小和成员的偏移量不会是微不足道的。 因此,你应该使用offsetof遇到麻烦。
我敢打赌,你用VC ++编译这个。 现在尝试用g ++,看看它是如何工作的…
长话短说,这是不确定的,但一些编译器可能会允许它。 其他人不。 无论如何,这是不可移植的。
在C ++中,你可以得到像这样的相对偏移量:
class A { public: int i; }; class B : public A { public: int i; }; void test() { printf("%p, %p\n", &A::i, &B::i); // edit: changed %x to %p }
这似乎对我很好:
#define myOffset(Class,Member) ({Class o; (size_t)&(o.Member) - (size_t)&o;})
为我工作
#define get_offset(type, member) ((size_t)(&((type*)(1))->member)-1) #define get_container(ptr, type, member) ((type *)((char *)(ptr) - get_offset(type, member)))