'offsetof'宏观中发生了什么?

时间:2009-10-03 08:26:09

标签: visual-c++ macros casting

Visual C ++ 2008 C运行时提供运算符'offsetof',实际上宏定义如下:

#define offsetof(s,m)   (size_t)&reinterpret_cast<const volatile char&>((((s *)0)->m))

这允许您计算类m中成员变量s的偏移量。

本声明中我不明白的是:

  1. 为什么我们将m投射到任何东西然后取消引用它?这不会有效吗?

    及(((S *)0) - &GT; M)

  2. 选择字符引用(char&)作为演员目标的原因是什么?

  3. 为什么要使用volatile?编译器是否存在优化m加载的危险?如果是这样,那会以什么样的方式发生?

5 个答案:

答案 0 :(得分:2)

偏移量以字节为单位。因此,要获得以字节表示的数字,必须将地址转换为char,因为它与字节大小相同(在此平台上)。

使用volatile可能是一个谨慎的步骤,以确保编译器优化(现在不存在或将来可能添加)将改变演员的精确含义。

<强>更新

如果我们看一下宏定义:

(size_t)&reinterpret_cast<const volatile char&>((((s *)0)->m))

删除了cast-to-char后,它将是:

(size_t)&((((s *)0)->m))

换句话说,在地址为零的对象中获取成员m的地址,乍一看似乎没问题。所以必须有某种方式可能会导致问题。

需要注意的一件事是,操作符&可能会在m碰巧的任何类型上重载。如果是这样,这个宏将在一个非常接近零地址的“人造”对象上执行任意代码。这可能会导致访问冲突。

这种滥用可能超出了offsetof的适用范围,它应该仅用于POD类型。也许这个想法是,最好返回一个垃圾值而不是崩溃。

(更新2:史蒂夫在评论中指出,operator ->没有类似的问题)

答案 1 :(得分:1)

在C ++中,

offsetof是非常小心的。这是C的遗物。这些天我们应该使用成员指针。也就是说,我认为数据成员的成员指针是过度设计和破坏的 - 我实际上更喜欢偏移。

即使如此,偏移也充满了令人讨厌的惊喜。

首先,对于您的具体问题,我怀疑真正的问题是它们相对于传统的C宏(我认为是C ++标准中强制要求)进行了调整。他们可能会使用reinterpret_cast来表示“它是C ++!”原因(为什么(size_t)演员?),以及char&amp;而不是char *尝试简化表达。

在这种形式下,转换为char看起来多余,但可能不是。 (size_t)不等同于reinterpret_cast,如果您尝试将指向其他类型的指针转​​换为整数,则会遇到问题。我不认为编译器甚至允许它,但说实话,我正在遭遇ATM内存故障。

char是单字节类型的事实在传统形式中具有一定的相关性,但这可能只是演员表再次正确的原因。说实话,我似乎记得铸造无效*,然后 char *。

顺便说一句,由于使用了C ++特定的东西,他们真的应该使用std :: ptrdiff_t进行最终的演员。

无论如何,回到令人讨厌的惊喜......

VC ++和GCC可能不会使用该宏。 IIRC,他们有一个内在的编译器,具体取决于选项。

原因是要做offsetof要做的事情,而不是宏做什么,这在C中是可靠的而在C ++中是不可靠的。要理解这一点,请考虑如果您的struct使用多个或虚拟继承会发生什么。在宏中,当您取消引用空指针时,最终会尝试访问地址为零的虚拟表指针,这意味着您的应用可能会崩溃。

出于这个原因,一些编译器具有仅使用指定的结构布局而不是尝试推导运行时类型的内在函数。但是C ++标准并没有强制要求甚至暗示这一点 - 它只是出于C兼容性的原因。如果你正在使用类heirarchies,你仍然需要小心,因为只要你使用多个或虚拟继承,就不能假设派生类的布局与基类的布局相匹配 - 你必须确保偏移量对完全运行时类型有效,而不仅仅是特定的基础。

如果您正在处理数据结构库,可能对节点使用单继承,但应用程序无法直接查看或使用您的节点,则offsetof效果很好。但严格地说,即便如此,也有一个问题。如果数据结构位于模板中,则节点可能包含具有模板参数类型的字段(包含的数据类型)。如果那不是POD,从技术上讲你的结构也不是POD。对offsetof的所有标准要求是它适用于POD。在实践中,它会工作 - 你的类型没有获得虚拟表或任何东西只是因为它有一个非POD成员 - 但你没有保证。

如果在使用字段偏移取消引用时知道确切的运行时类型,即使使用多个虚拟继承也应该没问题,但只有在编译器提供offsetof的内部实现以便首先导出该偏移时。我的建议 - 不要这样做。

为什么在数据结构库中使用继承?那么,怎么样......

class node_base                       { ... };
class leaf_node   : public node_base  { ... };
class branch_node : public node_base  { ... };

node_base中的字段在叶子和分支中自动共享(具有相同的布局),避免了C中的常见错误,意外地有不同的节点布局。

BTW - 这种东西可以避免偏移。即使您对某些作业使用offsetof,node_base仍然可以使用虚拟方法,因此只需要取消引用成员变量就可以使用虚拟表。因此,node_base可以具有纯虚拟getter,setter和其他方法。通常,这正是你应该做的。使用offsetof(或成员指针)是一个复杂问题,只有在您知道需要它时才应该用作优化。例如,如果您的数据结构位于磁盘文件中,那么您肯定不需要它 - 与磁盘访问开销相比,一些虚拟调用开销将是微不足道的,因此任何优化工作都应该用于最小化磁盘访问。

嗯 - 在那里切了一下。糟糕。

答案 2 :(得分:0)

char被保证是架构可以“咬”(也就是字节)的最小位数。

所有指针实际上都是数字,因此将地址0转换为该类型,因为它是开头。

从0开始获取成员的地址(结果为0 + location_of_m)。

将其转回size_t

答案 3 :(得分:0)

1)我也不知道为什么这样做。

2)char类型有两种特殊之处。

没有其他类型的对齐限制比char类型弱。这对于重新解释指针之间以及表达式和引用之间的转换非常重要。

它也是唯一的类型(连同其无符号变体),如果使用char来访问不同类型的变量的存储值,则规范定义了行为。我不知道这是否适用于这种特殊情况。

3)我认为volatile修饰符用于确保编译器优化不会导致尝试读取内存。

答案 4 :(得分:-1)

2。选择char reference(char&amp;)作为演员目标的原因是什么?

如果类型s有operator&amp;超载然后我们无法使用&amp; s

获取地址

所以我们将类型s reinterpret_cast重写为原始类型char,因为原始类型为char 没有运营商和重载

现在我们可以从那里得到地址

如果在C中则不需要reinterpret_cast

3。为何使用volatile?是否存在编译器优化m的加载的危险?如果是这样,那会以什么样的方式发生?

此处volatile与编译器优化无关。

如果类型s具有const或volatile或两个限定符 reinterpret_cast无法转换为char&amp;因为reinterpret_cast无法删除cv-qualifiers

所以结果是使用&lt; const volatile char&amp;&gt;从任何组合中投射作品