我正在寻找一种方法来获取非 POD 性质的C++类的数据成员的偏移量。
原因如下:
我想以HDF5格式存储数据,这似乎最适合我的材料类型(数值模拟输出),但它可能是一个相当面向C的库。我想通过 C++ 接口使用它,这将需要我声明这样的存储类型(遵循此处和此处的文档(第 4.3.2.1.1 节)):
class example {
public:
double member_a;
int member_b;
} //class example
H5::CompType func_that_creates_example_CompType() {
H5::CompType ct;
ct.insertMember("a", HOFFSET(example, member_a), H5::PredType::NATIVE_DOUBLE);
ct.insertMember("b", HOFFSET(example, member_b), H5::PredType::NATIVE_INT);
return ct;
} //func_that_creates_example_CompType
其中 HOFFSET 是使用 offset 的特定于 HDF 的宏。
当然,问题在于,一旦示例类变得更具功能性,它就不再是 POD 类型,因此使用 offsetof 将给出未定义的结果。
我能想到的唯一解决方法是首先将我要存储的数据导出到更简单的结构中,然后将其传递给 HDF。然而,这确实涉及数据复制,这正是HDF试图避免的(以及为什么他们有这个CompType,它使库能够进入你的对象以将其数据保存到文件中)。
所以我希望你能有更好的主意。理想情况下,我会为这个问题寻找一种便携式解决方法,但如果缺少这一点,您可以给我一个适用于 x86 的想法,并与 GCC x86_64,我已经非常感激了。
-----后附:-----
Greg Hewgill在下面建议将数据存储在一个简单的结构中,然后通过继承来构建实际的类。特别是对于HDF,我认为这可能实际上不起作用。比上面更详细的使用场景:
class base_pod {
public:
double member_a;
int member_b;
}; //class base_pod
class derived_non_pod : private base_pod {
public:
//the following method is only virtual to illustrate the problem
virtual double get_member_a() {return member_a; }
}; //class derived_non_pod
class that_uses_derived_non_pod {
public:
void whatever();
private:
derived_non_pod member_c;
}; //class that_uses_derived_non_pod
现在,当我们存储类that_uses_derived_non_pod的实例时,我们不能将其内存布局描述为base_pod member_c。这会弄错偏移量,因为derived_non_pod添加了时髦的东西(比如虚拟功能表,我猜?
Greg Hewgill的解决方案可能比这个更可取(也许是组合而不是继承)。
但是,我认为在 x86 和 x86_64 上使用 GCC,只要它"有意义",偏移量实际上也适用于非 POD 类型的成员。因此,例如,它不适用于从虚拟基类继承的成员,因为在 GCC 中,它是通过额外的间接实现的。但是,只要您坚持使用普通的公共单一继承,GCC 恰好以一种意味着每个成员都可以在对象指针的偏移量处访问的方式布置您的对象,因此偏移量实现将给出正确的答案。
当然,麻烦在于您必须忽略警告,这意味着如果您执行不起作用的操作,您将取消引用接近于空的指针。从好的方面来说,问题的原因在运行时可能很明显。在负的一面,哎呀。
[编辑:我刚刚在gcc 3.4.4上对此进行了测试,实际上,当获取从虚拟基类继承的成员的偏移量时,警告已升级为错误。这很好。我仍然有点担心 gcc 的未来版本(4,甚至,我不必手)会更加严格,如果你采用这种方法,你的代码将来可能会停止编译。
根据你想要的可移植性,你甚至可以在非 POD 类型上使用 offsetof()。它不是严格一致的,但以 offsetof() 在 gcc 和 MSVC 上实现的方式,它将在当前版本和最近的过去与非 POD 类型一起使用。
您可以在基类中声明 POD 类型,然后扩展该类(可能使用private
继承)以添加其他功能。
更新:由于derived_non_pod
的实例也可以被视为base_pod
,因此数据成员的偏移量必须相同。关于实现,编译器将在布局derived_non_pod
结构时在base_pod
字段之后分配 vtable 指针。
我突然想到,如果您使用私有继承,编译器可以选择对数据字段重新排序。然而,这样做不太可能,并且将遗产保护或公开将避免这种可能的陷阱。
确定Roel的回答以及对onebyone答案的考虑涵盖了您提出的大部分问题。
struct A
{
int i;
};
class B: public A
{
public:
virtual void foo ()
{
}
};
int main ()
{
std::cout << offsetof (B, A::i) << std::endl;
}
使用 g++,上面的输出 4,如果 B 在基类成员 'i' 之前有一个 vtable,这就是你所期望的。
不过,即使存在虚拟基数,也应该可以手动计算偏移量:
struct A1 {
int i;
};
struct A2 {
int j;
};
struct A3 : public virtual A2 {
};
class B: public A1, public A3 {
public:
virtual void foo () {
}
};
template <typename MostDerived, typename C, typename M>
ptrdiff_t calcOffset (M C::* member)
{
MostDerived d;
return reinterpret_cast<char*> (&(d.*member)) - reinterpret_cast<char*> (&d);
}
int main ()
{
B b;
std::cout << calcOffset<B> (&A2::j) << ", "
<< calcOffset<B> (&A1::i) << std::endl;
}
使用 g++,此程序输出 4 和 8。 同样,这与 vtable 一致,因为 B 的第一个成员后跟虚拟基 A2 及其成员 'j'。 最后是非虚拟基 A1 及其成员 'i'。
关键点是,您始终根据派生最多的对象(即 B. 如果成员是私有的,则可能需要为每个成员添加"getMyOffset"调用。 此调用将在名称可访问的位置执行计算。
您可能会发现以下内容也很有用。 我认为将所有这些与您为其构建 HDF 类型的对象相关联是很好的:
struct H5MemberDef
{
const char * member_name;
ptrdiff_t offset;
H5PredType h5_type;
};
class B // ....
{
public:
// ...
static H5memberDef memberDef[];
};
H5MemberDef B::memberDef[] = {
{ "i", calcOffset<B> (&A1::i), H5::PredType::NATIVE_INT }
, { "j", calcOffset<B> (&A2::j), H5::PredType::NATIVE_INT }
, { 0, 0, H5::PredType::NATIVE_INT }
};
然后你可以通过一个循环来构建H5type:
H5::CompType func_that_creates_example_CompType(H5MemberDef * pDef) {
H5::CompType ct;
while (*pDef->member_name != 0)
{
ct.insertMember(pDef->member_name, pDef->offset, pDef->h5_type);
++pDef;
}
return ct;
}
现在,如果将成员添加到 B 或其基数之一,则向此表添加简单内容将导致生成正确的 HDF 类型。
问题是一旦你的结构/类不是外部"C",C++编译器就可以自由地重新排列和优化你的结构/类的布局,所以你最终可能会得到一个重新排序的结构,这取决于你的编译器。
有类似 C 行为的预处理器(例如 #pragma 包)标志,但在大多数情况下它们不是可移植的。
使用指向成员的指针而不是 offsetof() 会起作用吗? 我知道您可能需要执行各种强制转换才能实际使用指针,因为我猜测 InsertMember 在运行时作用于最后一个参数中指定的类型。
但是使用您当前的解决方案,您已经在使用类型系统,所以我不确定您在那里会丢失任何东西。 除了指向成员的指针的语法很可怕。
这对我来说很好用,我不明白为什么它不会:
#define myOffset(Class,Member) ({Class o; (size_t)&(o.Member) - (size_t)&o;})