52

我正在研究如何在 C++ 中获取成员的内存偏移量,并在维基百科上遇到了这个问题:

在 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 数据结构是什么,要么我错过了其他一些难题。我不明白问题是什么。

4

11 回答 11

48

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

于 2009-07-15T11:03:48.827 回答
37

简短的回答:offsetof 是一个仅在 C++ 标准中用于遗留 C 兼容性的功能。因此,它基本上仅限于 C 中无法完成的内容。C++ 仅支持 C 兼容性所必需的内容。

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

结果是即使在没有标准支持的情况下,offsetof 也经常在 C++ 中工作(取决于使用的源代码和编译器)——除非它没有。所以你应该非常小心在 C++ 中使用 offsetof,特别是因为我不知道一个编译器会为非 POD 使用生成警告......offsetof如果在标准之外使用现代 GCC 和 Clang 将发出警告( -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 实际上会出错。

于 2009-07-15T08:03:52.480 回答
5

一般来说,当你问“为什么有些东西是未定义的”时,答案是“因为标准是这样说的”。通常,理性是基于一个或多个原因,例如:

  • 在这种情况下很难静态检测到您。

  • 特殊情况很难定义,没有人愿意为特殊情况下定义;

  • 它的使用主要由其他功能覆盖;

  • 标准化时的现有做法各不相同,破坏现有的实施和依赖它们的计划被认为比标准化更有害。

回到offsetof,第二个原因可能是一个占主导地位的原因。如果您查看 C++0X,该标准以前使用 POD,现在使用“标准布局”、“布局兼容”、“POD”,允许更精细的情况。offsetof 现在需要“标准布局”类,这是委员会不想强制布局的情况。

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

于 2009-07-15T10:14:03.480 回答
2

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

来自维基百科的 c++0x 文章

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

如果类/结构是普通的、标准布局的并且其所有非静态成员都是 POD,则将其视为 POD。

一个平凡的类或结构被定义为:

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

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

  1. 仅具有标准布局类型的非静态数据成员
  2. 对所有非静态成员具有相同的访问控制(公共、私有、受保护)
  3. 没有虚函数
  4. 没有虚拟基类
  5. 仅具有标准布局类型的基类
  6. 没有与第一个定义的非静态成员相同类型的基类
  7. 要么没有具有非静态成员的基类,要么在派生最多的类中没有非静态数据成员,并且最多有一个具有非静态成员的基类。本质上,此类层次结构中可能只有一个类具有非静态成员。
于 2009-07-15T08:50:28.547 回答
0

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

C++ 中的 POD 类型是什么?

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

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

于 2009-07-15T07:30:06.900 回答
-2

这每次都有效,它是在 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;
}

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

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

于 2018-08-16T19:35:05.220 回答
-3

例如,如果您添加一个虚拟的空析构函数:

virtual ~Foo() {}

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

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

于 2009-07-15T07:34:16.207 回答
-3

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

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

于 2009-07-15T07:37:41.160 回答
-3

为我工作

   #define get_offset(type, member) ((size_t)(&((type*)(1))->member)-1)
   #define get_container(ptr, type, member) ((type *)((char *)(ptr) - get_offset(type, member)))
于 2013-12-17T05:43:49.003 回答
-4

在 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
}
于 2009-07-15T07:35:10.807 回答
-4

这对我来说似乎很好用:

#define myOffset(Class,Member) ({Class o; (size_t)&(o.Member) - (size_t)&o;})
于 2013-04-29T01:59:36.090 回答