“container_of”宏可以严格遵守吗?

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', ie 'g == &f' 

但是,还不完全清楚container_of中container_of的减法是否被视为未定义的行为。

一方面,因为struct foo bar只是一个整数,所以只有*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 (请参阅此问题 )。 这意味着pq 比较相同,但在某些实现定义的方式中可以是不同的(这样只有q可以被解除引用)。 这可能意味着给出以下内容:

 // assume same 'struct foo' and 'f' declarations char *p = (char *)&f.bar; char *q = (char *)&f + offsetof(struct foo, bar); 

pq比较相同,但可能有不同的边界,因为(char *)的转换来自指向不兼容类型的指针。


总而言之,C标准并不完全清楚这种行为,并且试图应用标准的其他部分(或者至少我对它们的解释)会导致冲突。 那么,是否可以以严格一致的方式定义container_of ? 如果是,上述定义是否正确?


在对我对这个问题的回答发表评论之后, 这里讨论了 这一点

我认为它严格符合或标准中存在很大缺陷。 参考你的上一个例子,关于指针算术的部分并没有给编译器任何不同的处理pq余地。 它不取决于如何获得指针值,只取决于它指向的对象。

在指针算术中可以区别对待pq任何解释都需要解释pq不指向同一个对象。 由于在获得pq的方式上没有依赖于实现的行为,因此这意味着它们不会指向任何实现上的相同对象。 这反过来要求在所有实现中p == q为假,因此会使所有实际实现都不符合。

我只是想回答这个问题。

 int arr[5][5] = ... int *p = &arr[0][0] + 5; int *q = &arr[1][0]; 

这不是UB。 可以肯定的是,p是指向数组元素的指针,只提供它在边界内。 在每种情况下,它指向25个元素数组的第6个元素,并且可以安全地解除引用。 它也可以递增或递减以访问arrays的其他元素。

有关C ++,请参见n3797 S8.3.4。 C的措辞不同,但含义相同。 实际上,数组具有标准布局,并且在指针方面表现良好。


让我们想一下,事实并非如此。 有什么影响? 我们知道数组int [5] [5]的布局与int [25]相同,不能有填充,对齐或其他无关信息。 我们也知道,一旦p和q形成并给出一个值,它们在各方面都必须相同。

唯一的可能性是,如果标准说它是UB并且编译器编写器实现了标准,那么一个足够警惕的编译器可能(a)基于分析数据值发出诊断或(b)应用依赖的优化不偏离子arrays的边界。

有点不情愿我不得不承认(b)至少是一种可能性。 我被引导到一个相当奇怪的观察,如果你可以从编译器隐藏你的真实意图,这个代码可以保证产生定义的行为,但如果你在公开场合这样做,它可能不会。