linux内核(和其他地方)中常用的宏是container_of
,它基本上定义如下:
#define container_of(ptr, type, member) (((type) *)((char *)(ptr) - offsetof((type), (member))))
基本上允许在给定指向其中一个成员的指针的情况下恢复“父”结构:
struct foo {
char ch;
int bar;
};
...
struct foo f = ...
int *ptr = &f.bar; // 'ptr' points to the 'bar' member of 'struct foo' inside 'f'
struct foo *g = container_of(ptr, struct foo, bar);
// now, 'g' should point to 'f', i.e. 'g == &f'
但是,container_of
中包含的减法是否被视为未定义的行为并不完全清楚。
一方面,因为bar
内的struct foo
只是一个整数,所以只有*ptr
才有效(以及ptr + 1
)。因此,container_of
有效地生成类似ptr - sizeof(int)
的表达式,这是未定义的行为(即使没有解除引用)。
另一方面,C标准的第6.3.2.3节第7段规定,将指针转换为不同类型并再次返回将产生相同的指针。因此,“移动”指向struct foo
对象中间的指针,然后返回到开头应该生成原始指针。
主要关注的是允许实现在运行时检查越界索引。我对这个和前面提到的指针等价性要求的解释是必须在指针转换中保留边界(这包括指针衰减 - 否则,你怎么能使用指针迭代数组?)。虽然ptr
可能只是int
指针,ptr - 1
和*(ptr + 1)
都无效,ptr
仍然应该有一些中间概念结构,以便(char *)ptr - offsetof(struct foo, bar)
有效(即使指针在实践中等于ptr - 1
)。
最后,我发现了如果你有类似的事情:
int arr[5][5] = ...
int *p = &arr[0][0] + 5;
int *q = &arr[1][0];
虽然取消引用p
是未定义的行为,但指针本身是有效的,并且需要比较等于q
(请参阅this question)。这意味着p
和q
比较相同,但在某些实现定义的方式中可能会有所不同(这样只能解除引用q
)。这可能意味着给出以下内容:
// assume same 'struct foo' and 'f' declarations
char *p = (char *)&f.bar;
char *q = (char *)&f + offsetof(struct foo, bar);
p
和q
比较相同,但可能有不同的边界,因为(char *)
的强制转换来自指向不兼容类型的指针。
总而言之,C标准对于这种行为并不完全清楚,并且试图应用标准的其他部分(或者至少我对它们的解释)会导致冲突。那么,是否有可能以严格一致的方式定义container_of
?如果是,上述定义是否正确?
答案 0 :(得分:2)
我认为它严格符合或标准中存在很大缺陷。参考上一个例子,关于指针算术的部分没有给编译器任何余地来区别对待p
和q
。它不取决于如何获得指针值,而只取决于它指向的对象。
p
和q
在指针算术中可以区别对待的任何解释都需要p
和q
不指向同一对象的解释。由于在获取p
和q
方面没有依赖于实现的行为,因此这意味着它们不会指向任何实现上的相同对象。这反过来要求p == q
在所有实现中都是假的,因此会使所有实际实现都不符合。
答案 1 :(得分:0)
我只想回答这个问题。
int arr[5][5] = ...
int *p = &arr[0][0] + 5;
int *q = &arr[1][0];
这不是UB。可以肯定的是,p是指向数组元素的指针,只要它在边界内即可。在每种情况下,它指向25个元素数组的第6个元素,并且可以安全地解除引用。它也可以递增或递减以访问数组的其他元素。
有关C ++的信息,请参阅n3797 S8.3.4。 C的措辞不同,但含义相同。实际上,数组具有标准布局,并且在指针方面表现良好。
让我们暂时想一想,事实并非如此。有什么影响?我们知道数组int [5] [5]的布局与int [25]相同,不能有填充,对齐或其他无关信息。我们也知道,一旦p和q形成并给出一个值,它们在各个方面都必须相同。
唯一的可能性是,如果标准说它是UB并且编译器编写器实现了标准,那么一个足够警惕的编译器可能(a)基于分析数据值发出诊断或(b)应用优化这取决于不偏离子阵列的范围。
有点不情愿我不得不承认(b)至少是一种可能性。我被引导到一个相当奇怪的观察,如果你可以从编译器中隐藏你的真实意图,这个代码可以保证产生定义的行为,但是如果你在公开场合做它可能不会。