The container_of
and its WinApi equivalent CONTAINING_RECORD
are popular and useful macros. In principle, they use pointer arithmetic over char*
to recover a pointer to an aggregate to which a given pointer to the member belongs.
The minimalistic implementation is usually:
#define container_of(ptr, type, member) \
(type*)((char*)(ptr) - offsetof(type, member))
However, the strict compliance of a usage pattern of this macro is debatable. For example:
struct S {
int a;
int b;
};
int foo(void) {
struct S s = { .a = 42 };
int *p = &s.b;
struct S *q = container_of(p, struct S, b);
return q->a;
}
To my understanding, the program is not strictly compliant because:
s.b
is an l-value of type int
&s.b
is a pointer. Its value may carry implementation defined attributes
like a size of a value it is pointing to(char*)&s.b
does not do anything special to the potential metadata bound to the value of the pointer(char*)&s.b - offsetof(struct S, b)
, here UB is invoked because of pointer arithmetic
outside of the value that the pointer is pointing toI've noticed that the problem is not the container_of
macro itself. It is rather the way how ptr
argument is constructed.
If the pointer was computed from the l-value of struct S
type
then there would be no out-of-bounds arithmetic. There would be no UB.
A potentially compliant version of the program would be:
int foo(void) {
struct S s = { .a = 42 };
int *p = (int*)((char*)&s + offsetof(struct S, b));
struct S *q = container_of(p, struct S, b);
return q->a;
}
The actual arithmetic taking place is:
container_of(ptr, struct S, b)
Expand container_of
(struct S*)((char*)(ptr) - offsetof(struct S, b))
Place expression for ptr
(struct S*)((char*)((int*)((char*)&s + offsetof(struct S, b))) - offsetof(struct S, b))
Drop casts (char*)(int*)
(struct S*)((char*)&s + offsetof(struct S, b) - offsetof(struct S, b)))
Adding offsetof(struct S,b)
does not overflow struct S
. There is no UB when doing arithmetics.
The positive and negative terms are reduced.
(struct S*)((char*)&s)
Now drop redundant casts.
&s
Is the above derivation correct?
Is such a usage of container_of
strictly compliant?
If so, then the computation of a pointer to the member could be delegated to a new macro named member_of
.
The pointer can be constructed in a similar fashion as container_of
.
This new macro would be a complement of container_of
to be used in strictly compliant programs.
#define member_of(ptr, type, member) \
(void*)((char*)(ptr) + offsetof(type, member))
or a bit more convenient and typesafe but less portable (though fine in C23) version:
#define member_of(ptr, member) \
(typeof(&(ptr)->member))((char*)(ptr) + offsetof(typeof(*(ptr)), member))
The program would be:
int foo(void) {
struct S s = { .a = 42 };
int *p = member_of(&s, struct S, b);
struct S *q = container_of(p, struct S, b);
return q->a;
}
&s.b is a pointer. Its value that may carry implementation defined attributes like the size of a value it is pointing to
There are two cases of pointer metadata
Type #1 - Pointers point to allocated buffers where a hidden preamble holds metadata for the allocated block.
From this slideshow (slide #9 onwards):
This definitely doesn't affect pointer arithmetic and was not the case OP was referring to.
Type #2 - Provenance or other metadata embedded into the pointer
Here's the draft for "A Provenance-aware Memory Object Model for C". It describes the idea behind implementing pointer resolution provenance in C.
There's a quote discussing member offsets:
Pointer member offset Given a non-null pointer
p
at C type τ , which points to the start of a struct or union type object (ISO C suggests this has to exist, writing “The value is that of the named member of the object to which the first expression points”) with a memberm
, ifp
is(π, a)
, the result of offsetting the pointer to memberm
has the same provenance π and the suitably offset a.
Combined with two later statements about pointer arithmetic:
Pointer addition and subtraction Pointer arithmetic (addition or subtraction of integers) preserves provenance. The resulting pointer value is indeterminate if the result not within (or one-past) the storage instance.
Pointer difference Pointer difference is only defined for pointers with the same provenance and within the same array...
And the fact there are no proposals to change section 6.2.5 in the ISO standard that discusses pointer arithmetic.
Leads to the only possible conclusion, which is this is ok.
A different question would be whether or not the (char*)(ptr)
operation violates strict aliasing rules.
Strict aliasing definition (just in case), from a different stack overflow post:
"Strict aliasing is an assumption, made by the C (or C++) compiler, that dereferencing pointers to objects of different types will never refer to the same memory location (i.e. alias each other.)"
But because the operation is within the same struct and we only use it for compile-time calculations, this is ok.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With