__builtin_offsetof运算符的目的和返回类型是什么?

14

__builtin_offsetof 操作符 (或 Symbian 中的 _FOFF 操作符) 在 C++ 中的目的是什么?

此外,它返回什么?指针还是字节数?

4个回答

16

它是GCC编译器提供的内置函数,用于实现由C和C++标准指定的offsetof宏:

GCC - offsetof

它返回POD结构体/联合体成员所在的字节偏移量。

示例:

struct abc1 { int a, b, c; };
union abc2 { int a, b, c; };
struct abc3 { abc3() { } int a, b, c; }; // non-POD
union abc4 { abc4() { } int a, b, c; };  // non-POD

assert(offsetof(abc1, a) == 0); // always, because there's no padding before a.
assert(offsetof(abc1, b) == 4); // here, on my system
assert(offsetof(abc2, a) == offsetof(abc2, b)); // (members overlap)
assert(offsetof(abc3, c) == 8); // undefined behavior. GCC outputs warnings
assert(offsetof(abc4, a) == 0); // undefined behavior. GCC outputs warnings

@Jonathan提供了一个很好的例子,你可以在这里使用它。我记得曾经看到它被用于实现侵入式列表(其中数据项本身包括next和prev指针),但是可惜我不记得它在实现中有什么帮助了。


我猜它有用的地方在于被侵入的节点包含指向“下一个”对象中节点的指针。当使用列表时,您需要从节点到对象的基础部分,因此您需要从指针值中减去offsetof(something)字节并reinterpret_cast。 - Steve Jessop
当然,在C++中这些都是非常不可移植的,但在C中可以胜任。 - Steve Jessop

13

正如@litb指出并且@JesperE所展示的,offsetof() 提供了一个以字节为单位的整数偏移量(作为size_t值)。

什么时候会使用它?

其中一个可能相关的情况是表驱动操作,用于从文件中读取大量不同配置参数并将值塞入同样巨大的数据结构中。将“巨大”减少到 SO 级别(忽略一系列必要的现实实践,例如在头文件中定义结构类型),我的意思是有些参数可能是整数而其他参数则可能是字符串,代码可能看起来像这样:

#include <stddef.h>

typedef stuct config_info config_info;
struct config_info
{
   int parameter1;
   int parameter2;
   int parameter3;
   char *string1;
   char *string2;
   char *string3;
   int parameter4;
} main_configuration;

typedef struct config_desc config_desc;
static const struct config_desc
{
   char *name;
   enum paramtype { PT_INT, PT_STR } type;
   size_t offset;
   int   min_val;
   int   max_val;
   int   max_len;
} desc_configuration[] =
{
    { "GIZMOTRON_RATING", PT_INT, offsetof(config_info, parameter1), 0, 100, 0 },
    { "NECROSIS_FACTOR",  PT_INT, offsetof(config_info, parameter2), -20, +20, 0 },
    { "GILLYWEED_LEAVES", PT_INT, offsetof(config_info, parameter3), 1, 3, 0 },
    { "INFLATION_FACTOR", PT_INT, offsetof(config_info, parameter4), 1000, 10000, 0 },
    { "EXTRA_CONFIG",     PT_STR, offsetof(config_info, string1), 0, 0, 64 },
    { "USER_NAME",        PT_STR, offsetof(config_info, string2), 0, 0, 16 },
    { "GIZMOTRON_LABEL",  PT_STR, offsetof(config_info, string3), 0, 0, 32 },
};

现在可以编写一个通用函数,从配置文件中读取行,忽略注释和空行。然后它会隔离参数名称,在desc_configuration表格中查找(你可能需要对其进行排序以进行二分搜索 - 多个SO问题都涉及到了这个)。当它找到正确的config_desc记录时,它可以将找到的值和config_desc条目传递给两个例程之一 - 用于处理字符串的例程或用于处理整数的例程。

其中重要的部分是:

static int validate_set_int_config(const config_desc *desc, char *value)
{
    int *data = (int *)((char *)&main_configuration + desc->offset);
    ...
    *data = atoi(value);
    ...
}

static int validate_set_str_config(const config_desc *desc, char *value)
{
    char **data = (char **)((char *)&main_configuration + desc->offset);
    ...
    *data = strdup(value);
    ...
}

这样可以避免为结构中的每个成员编写单独的函数。


1
如果你想变得非常邪恶,你可以使用一个包含参数名称和在“desc_configuration”中的索引的哈希表。顺便说一下,这是一个真正惊人的例子。 - Robert S. Barnes
1
@Robert:这个例子紧密地基于从配置文件读取数据到大型数据结构中,然后反向进行的过程。我不会浪费时间解释它当前是如何完成的:可以说,有300个参数,在处理所有内容的函数中有约4500行代码,并且有很多重复。可悲的是,我不负责这段代码。 - Jonathan Leffler
另请参见:https://dev59.com/Y0nSa4cB1Zd3GeqPN2sv - Jonathan Leffler

5
内置的__offsetof运算符的目的是,编译器供应商可以继续定义offsetof()宏,并使其适用于定义了一元operator&的类。典型的C宏定义offsetof()仅在(&lvalue)返回该rvalue的地址时起作用。即:
#define offsetof(type, member) (int)(&((type *)0)->member) // C definition, not C++
struct CFoo {
    struct Evil {
        int operator&() { return 42; }
    };
    Evil foo;
};
ptrdiff_t t = offsetof(CFoo, foo); // Would call Evil::operator& and return 42

这很奇怪。运算符&被定义为类CFoo而不是指向该类的指针。因此,它被调用的原因不清楚。其次,“->”具有更高的优先级,“&”将应用于“成员”。显然,在GC++编译器中存在错误,并且他们用丑陋的__builtin遮盖了它,这个问题一直困扰着GCC。 - Noob
@Noname:& 返回一个指针,通常不会将其作为参数。请看这个例子,& 应用于 foo,它是一个对象。 - MSalters
首先,“通常”不适用于C ++,在其中您可以使用任何您想要的任何运算符进行重载。但是无论如何,与其他运算符相比,&运算符具有优先级。在C中,“&foo-> bar”将返回指向“bar”的指针。在C ++中,这将应用“&运算符”到“bar”,而不是到“foo”。此外,在“offsetof”的定义中写入了“(type*)”,这意味着0被强制转换为指针而不是类,因此CFoo的operator &不能在任何情况下被应用。 - Noob
@Noname:不正确。你不能重载“任何你想要的运算符”,至少对于内置类型是不行的。而且,与CFoo不同,CFoo*是一种内置类型(即指针)。此外,即使在C++中,语法规则(请查看您的书)也会忽略重载。运算符优先级是固定的。这意味着为了C兼容性,“&foo->bar”必须在有和没有运算符重载的情况下解析相同。 - MSalters
好的,我明白发生了什么。你写的例子很糟糕,让人感到困惑,我一直以为运算符&是针对CFoo定义的,而实际上它是针对包含类Evil定义的。但出于某种原因,你决定将该成员命名为“foo”。无论如何,我之前说的仍然成立,运算符&适用于“member”,而不适用于((type*)0)。 - Noob

3

如@litb所说:结构体/类成员的偏移量(以字节为单位)。在C ++中,有些情况下它是未定义的,在这种情况下编译器会发出警告。我记得,在C中实现它的一种方法(至少是)是执行

#define offsetof(type, member) (int)(&((type *)0)->member)

但我相信这里存在一些问题,但我会让有兴趣的读者指出...

1
未定义行为,即使在C语言中也是如此。甚至有多种原因:重新定义std宏和对NULL的解引用。尽管如此,在stdlib中很常见,因为它受到不同规则的约束。 - MSalters
@MSalters - JesperE 是正确的。请参见 Linux 内核源代码中 stddef.h 的定义:http://lxr.linux.no/#linux+v2.6.31/include/linux/stddef.h#L24 - Robert S. Barnes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接