为什么你不能使用offsetof on non

我正在研究如何在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是一个仅在C ++标准中用于传统C兼容性的功能。 因此它基本上被限制在比在C中可以完成的东西上。C ++只支持C兼容性必须的东西。

由于offsetof基本上是一种依赖于简单的支持C的内存模型的hack(实现为宏),所以它将远离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内部类B静态,而它的工作原理,当一个实例是可用的。 这是因为虚拟继承,基类的位置存储在查找表中。

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

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

还有一点需要注意:我不得不在这个例子中重新实现offsetof(这里是offset_s),因为当我为一个虚拟基类的字段调用offsetof时,GCC实际上会出错。


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的基类(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“知道”它的所有基类在哪里,它们总是保存在相同的偏移量处。 所以在这种安排中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只能包含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的开始。简单的继承,指针中的实际地址和我在图中指出的地址之间的偏移将永远不会被使用,除非编译器确定了对象的动态类型。 相反,它将始终通过元信息正确地到达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将永远不会与虚拟继承一起工作。


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

  • 在哪种情况下,你很难静态检测。

  • 角落案件难以界定,没有人为定义特殊案件而痛苦;

  • 它的使用大部分都被其他功能所覆盖;

  • 标准化时现有的做法各不相同,根据它们打破现有的实施和计划被认为对标准化更有害。

  • 回到抵消,第二个原因可能是占主导地位。 如果你看看标准之前使用POD的C ++ 0X,它现在使用“标准布局”,“兼容布局”,“POD”来允许更精确的情况。 现在抵消需要“标准布局”类,这是委员会不想强制布局的情况。

    你还必须考虑offsetof()的常见用法,即当你有一个指向对象的void *指针时,它将得到一个字段的值。 多重继承 - 虚拟与否 - 在使用上存在问题。

    链接地址: http://www.djcxy.com/p/40307.html

    上一篇: Why can't you use offsetof on non

    下一篇: Why should I prefer to use member initialization list?