Doubly Linked Lists in C
Doubly linked lists are a very useful data-structure. You can add and remove elements, and scan forwards and backwards all in constant time. This makes a list a useful "container" that can contain a group of things that may or may not need to be ordered. The question becomes what is the best way to make one in C?
The obvious way to create a doubly linked list is to add two pointers to a struct containing the data you wish to insert into the list. Something like:
This can work quite well. It is obviously flexible enough to handle the task. (You can even have more than one pair of pointers, allowing the data to exist in more than one list at a time.) The problem is that open-coding is error-prone. Every list operation on foo will have to be reimplemented, and this introduces the chance for bugs.
Another way to construct a doubly linked list is to make the container generic. We can use a
Since every user can have the same implementation, the above removes the code duplication problem. However, it suffers from some limitations compared to the original method. The first problem is that objects can only belong to one list at a time. (Depending on the organization of the program, this may or may not be an issue.) A second problem is inefficiency. To add an object to the list we now need to do a memory allocation. The extra overhead can be quite significant. Finally, the use of void * pointers isn't type-safe. The wrong kind of object might be accidentally added to the list, and the compiler can't warn us about that bug.
Fortunately, others have run into this issue, and there are solutions that are both type-safe, fast, and generic. However, we will need to investigate some macro magic to explore how they work. The first such implementation is due to the *BSD system, existing within the queue.h header. Within that header are multiple methods, but the most generic is the TAILQ set of macros. These define a macro to construct a generic list:
Given a name for the new structure and a type, it constructs a struct definition for the head of the list. List entries have a similar definition by macro:
To use the above, you invoke the macro from within the struct definition for your data:
The header then contains other macros that allow insertion, deletion, and iteration through the list. For example, the code to remove an element looks like:
The above is wrapped in a do-while(0) loop so that it acts like a single statement. This is to avoid dangling-else problems with if statements. The rest of the code is quite simple. It checks to see that the element is not last and if it is, it updates the list head structure instead of a previous element. The rest is just simple pointer operations to elide the unwanted element. Since every list uses the same embedded structure, the same operations to access tqe_next, and tqe_prev always work. You just need to remember to pass in the right "field" that corresponds to the wanted list. In the case of lists of type struct foo above, we would need to use 'my_list'.
The above works, but is slightly inefficient. Note how the list end needs to be treated separately via the if statement? If the list is circular, then all the insertion and removal operations are simplified. The *BSD header has a corresponding CIRCLEQ set of macros that do this. However, the Linux kernel developers have taken that idea further with their struct list_head based implementation.
If you look within the Linux kernel source code, there is a header that implements a generic doubly linked list at include/linux/list.h. It is somewhat "cleaner" than the BSD implementation since there is only one structure ever defined, the list_head:
You just include a field of this type within your data if you want to include it in a list:
The problem here is how can it work? The list_head structures point to each other... but how can you move from a pointer to a list_head, which could be at any offset in the data, to a pointer to the data itself? The Linux kernel does this via the container_of() macro, which in turn uses the rarely used offsetof() feature of the C standard. Offsetof(), when given a type, and a member of that type, will return the byte offset of that member from the start of the type. With that information, you can do a small amount of pointer arithmetic to shift from a pointer to a contained member to a pointer to the containing object.
In the kernel container_of() is implemented as:
The first line is just there for type-checking. We want to make sure that the pointer passed in is really a pointer that has the type of &(type.member). The second line just converts that pointer to (char *) so we can use the byte-sized offset. The final cast returns us to the correct type.
The list code then uses a simple wrapper called "list_entry" to go from pointers to entries, to pointers to elements:
Finally, iterator macros can be constructed that use list_entry to get the elements one at a time. An example is list_for_each_entry(), which allows the user to iterate over the whole list:
Just like the TAILQ macros, we need to give the list member (field) so that the right list is iterated over.
The above techniques are quite interesting... but can perhaps be improved. The first problem is that many of the macros need to give the data structure member or field, and if not, deal directly with the "confusing" list_head's instead. Another problem is that these lists aren't quite as type safe as they could be. Very little prevents the wrong kind of list_head from being used. You can put things of different type in the same list, and then accidentally iterate over them. If the offsets of the list_head fields are different, then data corruption will be the result.
Fortunately, these issues can be overcome at the cost of making the list head more like the *BSD TAILQ technique. We would like to store the type of the list elements, and also the numeric value of the field offset within the list head structure. This sounds impossible, but with gcc extensions we can actually get this to work. The magic definition looks like:
We use a generic definition like struct list_head, but this time called struct dlist. However, we also require the user to declare the head to be a separate type. This new type is a little weird, it is a union between a struct dlist, combined with a pointer to some extra data. The "data" pointer will never actually be read from or stored to... it just is a sneaky way for us to store some information within the type. We store the offset of the field for the list, together with the type of the elements of the list. These two pieces of information can be extracted via the macros:
Note that the sizeof() intrinsic, together with the gcc extension typeof() intrinsic both don't actually evaluate their arguments. They just use type information, so the data pointer isn't actually followed. You can use this method to "store" arbitrary types and unsigned long integers within another type. Provided the original type is the same size or larger than a pointer, there is no runtime overhead cost. We deliberately make the data pointer an unnamed union to prevent aliasing issues. This is because we don't want potential compiler optimizations impacted by this trick.
The above means that we can convert between pointers to list elements and entries without having to pass the 'field', provided we have a way to access the type of the list head. Since many list operations need the list head anyway, this can be a huge win for simplifying the user interface to the list data-structure. Macros that do this are:
(In this case we don't do the nice extra type-checking that the Linux kernel does, but it could be easily added. One way to do this might be:
This checks to see if elem is really the type of the things stored in the list pointed to by head.
We can create "unsafe" iterators for our generic list:
These aren't quite right because they might try to interpret the list head structure as an element if we iterate off of the end of the list. To avoid that, we need to know if the list is empty:
Or, if we are either the first or last element, and iterating the wrong way:
The other code to insert and remove elements is very similar to the list_head method. However, we can use some of the previously introduced infrastructure to improve the list iterators. Basically, we can avoid having to pass in the field, and instead calculate it from the type of the list head. Code that does this for forward and reverse iteration looks like:
Finally, it is possible to improve the "safe" iterator, that still works if the current element is deleted. The original list_head version requires that the user pass in a temporary variable to store the next iteration in. However, it is possible to construct our own variable to use. A way to do this is to use the fact that a for loop in C99 allows variable declarations within the for-loop parenthesis. Unfortunately, a simple loop doesn't quite do what we want, but it is possible to fix that with a couple of if statements and a goto:
The resulting macros, whilst much more complex to write, are much much simpler to use. The result is very type safe, and the resulting object code is identical to the list_head method. The trick of storing information within the container type can also be used with other data structures. Quite a bit of the power of C++ templates can actually be used in C this way.
Company Info |
Product Index |
Category Index |
Copyright © Lockless Inc All Rights Reserved.