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

时间:2009-07-15 07:21:58

标签: c++ 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数据结构是什么,或者我错过了其他一些难题。我不知道问题是什么。

11 个答案:

答案 0 :(得分:42)

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 永远不会使用虚拟继承。

答案 1 :(得分:35)

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

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

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

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

#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内找到字段B时会崩溃,而在实例可用时它会起作用。这是因为虚拟继承,其中基类的位置存储在查找表中。

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

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

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

答案 2 :(得分:6)

一般来说,当你问“为什么是未定义的”时,答案是“因为标准是这样的”。通常,理性是出于以下一个或多个原因:

  • 在您遇到的情况下,很难静态检测。

  • 极端情况难以界定,没有人承担定义特殊情况的痛苦;

  • 其使用主要由其他功能涵盖;

  • 标准化时的现有做法各不相同,违反现有的实施方案和程序取决于它们被认为比标准化更有害。

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

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

答案 3 :(得分:2)

我认为你的类符合POD的c ++ 0x定义。 g ++在其最新版本中实现了一些c ++ 0x。我认为VS2008中还有一些c ++ 0x位。

来自wikipedia's c++0x article

  

C ++ 0x将放宽关于POD定义的几条规则。

     

如果类/结构是微不足道的,标准布局,则它被认为是POD   如果它的所有非静态成员都是   吊舱。

     

定义了一个简单的类或结构   作为一个:

     
      
  1. 有一个简单的默认构造函数。这可以使用默认值   构造函数语法(SomeConstructor()   = default;)。
  2.   
  3. 有一个简单的复制构造函数,可以使用默认语法。
  4.   
  5. 有一个普通的复制赋值运算符,可以使用默认值   语法。
  6.   
  7. 有一个简单的析构函数,它不能是虚拟的。
  8.         

    标准布局类或结构是   定义为:

         
        
    1. 仅包含标准布局类型的非静态数据成员
    2.   
    3. 为所有人提供相同的访问控制(公共,私有,受保护)   非静态成员
    4.   
    5. 没有虚拟功能
    6.   
    7. 没有虚拟基类
    8.   
    9. 只有标准布局类型的基类
    10.   
    11. 没有与第一个定义的非静态相同类型的基类   构件
    12.   
    13. 要么没有非静态成员的基类,要么没有   非静态数据成员中最多的   派生类,最多一个基础   具有非静态成员的类。在   本质上,可能只有一个班级   在这个类的层次结构中   非静态成员。
    14.   

答案 4 :(得分:1)

关于POD数据结构的定义,请在这里解释[已经在Stack Overflow的另一篇文章中发布]

What are POD types in C++?

现在,来到您的代码,它正如预期的那样正常工作。这是因为,您正在尝试为您的类的公共成员找到offsetof(),这是有效的。

请让我知道,如果我的观点不正确,那么正确的问题并不能澄清你的疑问。

答案 5 :(得分:0)

这每次都有效,并且是c和c ++中使用最方便的版本

#define offset_start(s) s
#define offset_end(e) e
#define relative_offset(obj, start, end) ((int64_t)&obj->offset_end(end)-(int64_t)&obj->offset_start(start))

struct Test {
     int a;
     double b;
     Test* c;
     long d;
 }


int main() {
    Test t;
    cout << "a " << relative_offset((&t), a, a) << endl;
    cout << "b " << relative_offset((&t), a, b) << endl;
    cout << "c " << relative_offset((&t), a, c) << endl;
    cout << "d " << relative_offset((&t), a, d) << endl;
    return 0;
}

上面的代码仅要求您保存某个对象的实例,无论是结构还是类。然后,您需要传递指向类或结构的指针引用,以获取对其字段的访问权限。为确保获得正确的偏移量,切勿将“开始”字段设置在“结束”字段下方。我们使用编译器找出运行时的地址偏移量。

这使您不必担心编译器填充数据等问题。

答案 6 :(得分:-1)

例如,如果添加虚拟空析构函数:

virtual ~Foo() {}

您的类将变为“多态”,即它将具有隐藏的成员字段,该字段是指向包含指向虚函数的指针的“vtable”的指针。

由于隐藏的成员字段,对象的大小和成员的偏移量不会是微不足道的。因此,使用offsetof会遇到麻烦。

答案 7 :(得分:-1)

为我工作

   #define get_offset(type, member) ((size_t)(&((type*)(1))->member)-1)
   #define get_container(ptr, type, member) ((type *)((char *)(ptr) - get_offset(type, member)))

答案 8 :(得分:-2)

在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
}

答案 9 :(得分:-2)

我打赌你用VC ++编译它。现在尝试使用g ++,看看它是如何工作的......

长话短说,它是未定义的,但有些编译器可能允许它。其他人没有。在任何情况下,它都是非便携式的。

答案 10 :(得分:-2)

这似乎对我很好:

#define myOffset(Class,Member) ({Class o; (size_t)&(o.Member) - (size_t)&o;})