在查看Linux内核的双链接循环列表实现时,我发现了以下宏:
#define container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );})
它的工作方式是,它返回指向只给定其成员之一地址的结构的指针:
struct blabla
{
int value;
struct list_head *list;
}
这样,只要只给列表一个指针,就可以得到指向blabla的指针(并得到"value")。对于我的问题,我该如何使其尽可能便携(最好符合C89/C99?)。由于使用了typeof(),这只是gcc。
这就是我目前所掌握的:
#define container_of(ptr, type, member) (
(type *) (char *)(ptr)-offsetof(type,member)
)
这个片段是否符合ISO标准(因此应该能够在任何符合标准的编译器上编译)?
正如Ouah所评论的,({ ... })
语句表达式是GNU扩展;你将无法使用它。您的核心表达式接近所需内容,但没有足够的括号:
#define container_of(ptr, type, member)
((type *) ((char *)(ptr) - offsetof(type, member)))
在我看来,这是清白的。它只分布在SO的两条线上。
宏是按照对ptr
执行类型检查的方式编写的。如果编译器与gcc不兼容,可以使用复合文字而不是语句表达式,并返回到简单的指针检查,而不是使用__typeof__
:
#ifdef __GNUC__
#define member_type(type, member) __typeof__ (((type *)0)->member)
#else
#define member_type(type, member) const void
#endif
#define container_of(ptr, type, member) ((type *)(
(char *)(member_type(type, member) *){ ptr } - offsetof(type, member)))
ISO C90兼容版本,带类型检查。(但是,注意:ptr
的两个评估!)
#define container_of(ptr, type, member)
((type *) ((char *) (ptr) - offsetof(type, member) +
(&((type *) 0)->member == (ptr)) * 0))
struct container {
int dummy;
int memb;
};
#include <stddef.h>
#include <stdio.h>
int main()
{
struct container c;
int *p = &c.memb;
double *q = (double *) p;
struct container *pc = container_of(p, struct container, memb);
struct container *qc = container_of(q, struct container, memb);
return 0;
}
测试:
$ gcc -Wall containerof.c
containerof.c: In function ‘main’:
containerof.c:20:26: warning: comparison of distinct pointer types lacks a cast
containerof.c:20:21: warning: unused variable ‘qc’
containerof.c:19:21: warning: unused variable ‘pc’
我们得到的distinct pointer types
警告是26,但不是25。这是我们对指针被滥用的诊断。
我首先尝试将类型检查放在逗号运算符的左侧,gcc抱怨说这没有效果,这很麻烦。但是,通过将其作为操作数,我们可以确保它得到使用。
&((type *) 0)->member
技巧在ISO C中没有得到很好的定义,但它被广泛用于定义offsetof
。如果编译器对offsetof
使用这种空指针技巧,那么它几乎肯定会在自己的宏中表现出来。
是的,您可以制作"container_ of";宏严格符合ISO C。要做到这一点,你需要两件事:
-
去掉GNU扩展;
-
找到检查类型兼容性的方法。
基本上,类型检查不是运行时操作,而是编译时操作。而且我看不出有什么原因,为什么原创的"container_ of";实现创建新变量只是为了分配它并执行类型检查。这可以在不在某些表达式中创建新变量的情况下完成,这些表达式仅在编译时计算(并检查类型)。幸运的是,我们在C中没有太多的选择,唯一的选择是使用"sizeof(表达式)";以检查类型。参见示例:
#define container_of(ptr, type, member)
( (void)sizeof(0 ? (ptr) : &((type *)0)->member),
(type *)((char*)(ptr) - offsetof(type, member)) )
在第一行中,检查类型的兼容性(对于三元运算符,编译器必须确保类型可以转换为通用类型,或者两种类型都兼容)。第二行与原来的";container_ of";宏。
你可以在GodBolt上玩测试程序(https://godbolt.org/z/MncvzWfYn)并确保这种符合ISO的变体即使在微软的Visual Studio编译器中也能工作。
PS:过了一段时间,我发现以下变体可以更好:
#define CONTAINER_OF(ptr, type, member)
( (void)sizeof(0 ? (ptr) : &((type*)0)->member),
(typeof(_Generic((typeof(ptr))0, const typeof(*(typeof(ptr))0)*: (const type*)0, default: (type*)0)))
((uintptr_t)(const void*)(ptr) - offsetof(type, member)) )
不同之处在于,它保留了ptr
中的const
限定符,并将其分配给结果,例如:
- 如果
ptr
参数是const struct *
指针,则无论type
是否为常量,结果都将具有const type *
类型 - 如果
ptr
参数是非常量指针(struct*
),则结果将具有类型type*
,根据type
参数的类型,该类型可以是常量或非常量
因此,当指向某个结构的常量指针通过container_of
宏转换为非常量指针时,保留const
限定符可以降低出错的可能性。
不幸的是,对于C标准的早期版本,此版本需要C23或非标准typeof()
运算符。
与从Linux内核实现相反,ISO兼容container_of
宏的另一个原因是后者使用";语句表达式";GCC扩展,在参数ptr
是临时变量的情况下工作不好。当container_of
宏应用于函数调用的结果(这里假设container_of(func().x, struct y, m)
,func()
返回一个结构,其中x
是一个结构数组)或应用于复合语句(container_of((&(struct S){...}), struct B, m)
)时,可能会发生后者。在这两种情况下,对从linux借用的container_of
宏的调用都会导致一个悬空指针发生这种情况是因为作为ptr
参数传递的临时对象将在第一个分号之后(在container_of
宏的Linux实现的第一行)被销毁,并且因为由复合语句表达式创建的变量将在最近的块的末尾被销毁,该块是";语句表达式";它本身符合ISO的container_of
宏的实现没有这样的问题。