Yes, you can make "container_of" macros to be strictly ISO C conforming. To do this you need two things:
take rid of GNU exensions;
find a way to check types compatibility.
Basically, types checking is not run time operation, but compile time rather. And I not see any reasons, why original "container_of" implementation creates new variable just to assign it and perform type checking. This can be done without creation of new variable in some expression which is only computed (and types checked) in compile time. Fortunately, we have no much options in C and only choice is to use "sizeof(expression)" to check the type. See an example:
#define container_of(ptr, type, member) \
( (void)sizeof(0 ? (ptr) : &((type *)0)->member), \
(type *)((char*)(ptr) - offsetof(type, member)) )
In first line types compatibility is checked (for ternary operator compiler must insure, that types might be converted to common type, or that both types are compatible). Second line is the same, as in original "container_of" macros.
You can play with test program on GodBolt (https://godbolt.org/z/MncvzWfYn) and make sure, that this ISO conforming variant works even in Microsoft's Visual Studio compiler.
PS: After some time, I found that the following variant can be better:
#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)) )
The difference, is that it preserves const
qualifier from ptr
and assigns it to the result, for example:
- if
ptr
argument is const struct *
pointer, the result then will have a type of const type *
, despite if the type
is const or not;
- if
ptr
argument is non-const pointer (struct*
), the result then will have type type*
, which may be const or non-const, depending on the type of type
argument.
As a result, preserving const
qualifier reduces the possibility of the errors, when const pointer to some structure translated into non-const pointer via container_of
macro.
Unfortunately, this version requires C23 or non-standard typeof()
operator for earlier versions of C standard.
Another reason for ISO-compiant container_of
macro, as opposed to implementation from Linux kernel, is that latter uses "statement expression" GCC-extension which works badly in a case, when argument ptr
is a temporary variable. The latter might happen, when container_of
macro is applied to the result of a function invocation (container_of(func().x, struct y, m)
, here is assumed, that func()
returns a structure in which x
is an array of structures), or to the compound statement (container_of((&(struct S){...}), struct B, m)
). In both of these cases, a call to container_of
macro borrowed from linux will result in a dangling pointer! This happens because a temporary object passed as ptr
argument will be destroyed after the first semicolon (at the first line of Linux implementation of container_of
macro), and because a variable created by a compound statement expression will be destroyed at the end of the nearest block, which is "statement expression" itself. ISO-compliant implementation of container_of
macro have no such issues.