首页 文章

什么's going on in the '偏移'宏?

提问于
浏览
2

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

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

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

我在本声明中不明白的是:

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

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

  • 选择char引用( char& )作为强制转换目标的原因是什么?

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

5 回答

  • -1

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

    使用volatile可能是一个谨慎的步骤,以确保没有编译器优化(现在存在或将来可能添加)将改变强制转换的精确含义 .

    Update:

    如果我们看一下宏定义:

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

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

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

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

    我想到的一件事是,运算符 & 可能会在 m 碰巧的任何类型上重载 . 如果是这样,这个宏将在"artificial"对象上执行任意代码,该对象非常接近零地址 . 这可能会导致访问冲突 .

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

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

  • 1

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

    即便如此,offsetof充满了令人讨厌的惊喜 .

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

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

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

    顺便说一下,由于使用了C-specific的东西,他们真的应该使用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

    char 被保证是架构可以最小的位数"bite"(又称字节) .

    所有指针实际上都是数字,所以将地址0添加到该类型,因为它是开头 .

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

    把它转回 size_t .

  • 0

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

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

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

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

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

  • 0

    2 . What's the reason for choosing char reference (char&) as the cast target?

    如果类型s有运算符和重载,那么我们无法使用&s获取地址

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

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

    如果在C中则不需要reinterpret_cast

    3 . Why use volatile? Is there a danger of the compiler optimizing the loading of m? If so, in what exact way could that happen?

    这里volatile与编译器优化无关 .

    如果类型s具有const或volatile或两个限定符,则reinterpret_cast无法转换为char,因为reinterpret_cast无法删除cv限定符

    所以结果是使用<const volatile char&>从任何组合中进行投射工作

相关问题