On 2021-03-09, Ben Bacarisse <
ben.u...@bsb.me.uk> wrote:
> Kaz Kylheku <
563-36...@kylheku.com> writes:
>
>> On 2021-03-09, Chris M. Thomasson <
chris.m.t...@gmail.com> wrote:
>>> How many people here use it?
>>
>> If you put the contained structure as the first member, you don't need
>> this; just a straight cast.
>
> But you then can't write generic list algorithms, no?
The intrusive container approach for generic containers, though it has
some nice characteristics with regard to memory allocation, is very
limited.
Firstly, by its very nature, it is incompatible with functional
programming. Objects must be mutated in order to put them into a list.
Secondly, once an object is on a list, it cannot be simultaneously put
into another list. (Unless, of course, it has more list nodes, which
is where we run into container_of).
The "object on one list at a time" has basically one text book use case:
the OS scheduler. A process in some state, one state at a time. States
have queues associated with them, and so a process moves between
different queues. For that, it can have links built into it, and so
no allocation takes place.
This is not a "generic container" situation though.
>> That takes care of probably more than the proverbial 95% of the
>> situations.
>
> Maybe such generic code is all in the 5%. I would argue percentages if
> that's what you are saying.
If I needed a generic container with multiple intrusive node links
today, I would spend the bytes and put the numeric offset into the link
node structure.
The offsetof macro is error prone. If a structure has several list
nodes:
struct foo {
lnode_t anode;
lnode_t bnode;
lnode_t cnode;
}
If we have a pointer ptr, to say, the cnode, but we accidentally do
this:
container_of(ptr, struct foo, bnode)
we have a critical error: the returned pointer is wrong; it points
somewhere before the structure.
If node has an offset to the parent structure, we can have a different
inline function or macro for that, just:
container_of(ptr, struct foo)
this retrieves ptr->offset, subtracts it from the char * version of ptr,
and casts it to struct foo *.
That also allows us to write generic code (which we should be concerned
with if our motivation is "generic containers"). We can write a single
common function which takes a pointer to any list node, whether it be
the object's anode, bnode or cathode (EE joke, sorry) and easily
converts it to the struct foo *, to then operate on that object.
We correctly initialize ptr->offset in one place.
We could have a ptr->parent poiner instead, but ptr->offset has
the virtue of being relative. If we assign one initialized structure
to another, the offsets work correctly in the copied structure,
whereas back-pointers will not:
*pfooa = *pfoob;
Offsets can be packed into smaller fields though. E.g. on 64 bit,
we burn 8 bytes for a parent pointer, but there could be situations
in which a smaller type could be used for an offset to some
advantage, alignment of surroundings permitting.