为什么不能在C++中的非POD结构上使用offsetof?

Ale*_*lex 49 c++ offsetof

我正在研究如何在C++中将成员的内存偏移量转换为类,并在维基百科上看到了这一点:

在C++代码中,您不能使用offsetof来访问非Plain Data Data Structures的结构或类的成员.

我尝试了它似乎工作正常.

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;
}
Run Code Online (Sandbox Code Playgroud)

我收到了一些警告,但它已经编译,运行时它给出了合理的输出:

Laptop:test alex$ ./test
4 8 12
Run Code Online (Sandbox Code Playgroud)

我想我要么误解POD数据结构是什么,要么我错过了其他一些难题.我不知道问题是什么.

Ste*_*sop 44

Bluehorn的答案是正确的,但对我而言,它并没有用最简单的术语解释问题的原因.我理解的方式如下:

如果NonPOD是非POD类,那么当你这样做时:

NonPOD np;
np.field;
Run Code Online (Sandbox Code Playgroud)

编译器不一定通过向基指针添加一些偏移量并解除引用来访问该字段.对于POD类,C++标准约束它(或类似的东西),但对于非POD类,它不会.编译器可能会改为从对象中读取指针,向该值添加偏移量以给出字段的存储位置,然后取消引用.如果该字段是NonPOD的虚拟基础的成员,则这是具有虚拟继承的常见机制.但它并不局限于这种情况.编译器可以做任何喜欢的事情.如果需要,它可以调用隐藏的编译器生成的虚拟成员函数.

在复杂的情况下,显然不可能将字段的位置表示为整数偏移量.因此offsetof在非POD类上无效.

如果您的编译器恰好以简单的方式存储对象(例如单继承,通常甚至是非虚拟多重继承,并且通常在您引用该对象的类中定义的字段,而不是在一些基类),然后它会恰好工作.有可能恰好在每一个编译器上工作的情况.这不会使它有效.

附录:虚拟继承如何工作?

使用简单继承,如果B是从A派生的,通常的实现是指向B的指针只是指向A的指针,B的附加数据粘在最后:

A* ---> field of A  <--- B*
        field of A
        field of B
Run Code Online (Sandbox Code Playgroud)

使用简单的多重继承,通常假设B的基类(调用'em A1和A2)按照B特有的顺序排列.但是指针的相同技巧不起作用:

A1* ---> field of A1
         field of A1
A2* ---> field of A2
         field of A2
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

然后将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
Run Code Online (Sandbox Code Playgroud)

但坚持下去.当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
Run Code Online (Sandbox Code Playgroud)

虽然我已经指出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
Run Code Online (Sandbox Code Playgroud)

因此,使用指针或B2的引用来访问A的字段需要的不仅仅是应用偏移量.我们必须读取B2的"A指针"字段,然后跟随它,然后才应用偏移量,因为取决于B2类的基础,该指针将具有不同的值.没有这样的事情offsetof(B2,field of A):没有.对于任何实现,offsetof 永远不会与虚拟继承一起使用.


Blu*_*orn 36

简短回答:offsetof是一种仅在C++标准中用于传统C兼容性的功能.因此它基本上仅限于C中的内容.C++仅支持C兼容性所必需的内容.

由于offsetof基本上是一个依赖于支持C的简单内存模型的hack(实现为宏),因此从C++编译器实现者那里如何组织类实例布局需要很多自由.

结果是,即使没有标准支持,偏移量通常也会在C++中起作用(取决于所使用的源代码和编译器) - 除非它没有.所以你应该非常小心在C++中使用offsetof,特别是因为我不知道会产生非POD使用警告的单个编译器......如果offsetof在标准(-Winvalid-offsetof)之外使用,现代GCC和Clang将发出警告.

编辑:正如您所要求的那样,以下内容可能会澄清问题:

#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;
}
Run Code Online (Sandbox Code Playgroud)

尝试aB静态类型中定位字段时会崩溃,而在实例可用时它会起作用.这是因为虚拟继承,其中基类的位置存储在查找表中.

虽然这是一个人为的例子,但实现可以使用查找表来查找类实例的公共,受保护和私有部分.或者使查找完全动态化(使用字段的哈希表)等.

标准只是通过将offsetof限制为POD而使所有可能性保持开放(IOW:没有办法为POD结构使用哈希表... :)

另一个注意事项:我必须为此示例重新实现offsetof(此处为:offset_s),因为当我为虚拟基类的字段调用offsetof时,GCC实际上会出错.

  • 取消引用成员会为您提供一个对象的一个​​成员的地址.offsetof()适用于某种类型.因此,如果偏移量在相同类型的对象之间不同,则会中断.很难相信这可能吗?考虑相同类型的自由对象和基础部件对象. (3认同)
  • 较新版本的g ++将在非POD使用时生成警告.见http://gcc.gnu.org/onlinedocs/gcc-4.4.0/gcc/Warning-Options.html#index-Winvalid_002doffsetof-441 (2认同)
  • 实际上,如果没有虚拟继承,使用 `offsetof` 仍然没有问题。即,如果确实所有偏移量在编译时都是已知的。 (2认同)

APr*_*mer 6

一般来说,当你问" 为什么不确定 "时,答案就是" 因为标准是这样说的 ".通常,理性是出于以下一个或多个原因:

  • 在你遇到的情况下,很难静态检测.

  • 角落案件难以界定,没有人承担定义特殊案件的痛苦;

  • 其用途主要由其他功能覆盖;

  • 标准化时的现有做法各不相同,违反现有的实施和程序,被认为比标准化更有害.

回到偏移,第二个原因可能是主导原因.如果你看看标准以前使用POD的C++ 0X,它现在使用"标准布局","布局兼容","POD"允许更精细的案例.而offsetof现在需要"标准布局"类,这是委员会不想强制布局的情况.

您还必须考虑offsetof()的常见用法,即当您有一个指向对象的void*指针时获取字段的值.多重继承 - 虚拟或非虚拟 - 对于该用途而言是有问题的.