Introduction
Linked lists are a core building block in C, but writing them from scratch often feels painful — endless pointer juggling, boilerplate code, and subtle bugs that waste hours. That’s where utlist.h changes the game. This tiny, single-header library gives you a complete set of macros for creating and managing linked lists that are both fast and elegant. Instead of reinventing the wheel, you can build, traverse, and manipulate lists with just a few lines of clean, readable code. In this guide, we’ll explore how utlist.h works, why it’s a hidden gem for C developers, and how you can use it to implement ruthlessly efficient lists without the headaches.
How utlist.h Works
At its core, utlist.h gives you a powerful set of macros that wrap the tedious parts of linked list management in C. To get started, you simply embed a next (and optionally prev for doubly-linked lists) pointer inside your struct. With that in place, you gain access to macros like DL_APPEND, DL_FOREACH, and DL_DELETE that handle insertion, iteration, and removal at lightning speed. Because these are preprocessor macros, they expand directly into pointer operations with almost zero overhead, giving you both the speed of raw C and the convenience of high-level utilities. Instead of hand-wiring pointer logic, you write a single line and let utlist.h do the work.
Why utlist.h Is a Hidden Gem for C Developers
C programmers often accept that linked lists mean boilerplate: endless node definitions, custom insert functions, and tricky edge-case handling. That’s exactly why utlist.h feels like a secret weapon. It strips away the noise and lets you focus on what your program actually does, not how to wire the pointers. The library is a single header file with no external dependencies, making it small, portable, and easy to drop into any project. Even better, it stays close to the metal: you still get raw pointer control and memory ownership, but without wasting time on repetitive list code. For developers who want both speed and clarity, utlist.h delivers a rare combination that makes it a true hidden gem in the C ecosystem.
Utlist Examples
Singly-Linked List Example
In this example, we define a simple struct with an id field and a next pointer, then use LL_APPEND to build a singly-linked list. With LL_FOREACH, we loop through the list and print each element. Finally, we clean up by iterating safely with LL_FOREACH_SAFE and deleting each node using LL_DELETE. This demonstrates how utlist.h removes the boilerplate of pointer juggling and makes list operations almost effortless.
// slist.c
#include <stdio.h>
#include <stdlib.h>
#include "utlist.h"
typedef struct node {
int id;
struct node *next; // required for singly-linked list
} node_t;
int main(void) {
node_t *list = NULL, *n, *tmp;
// ADD elements
for (int i = 1; i <= 3; i++) {
n = malloc(sizeof(*n));
n->id = i;
n->next = NULL;
LL_APPEND(list, n); // append to singly-linked list
}
// ITERATE
LL_FOREACH(list, n) {
printf("id=%d\n", n->id);
}
// DELETE ALL
LL_FOREACH_SAFE(list, n, tmp) {
LL_DELETE(list, n);
free(n);
}
return 0;
}
Doubly-Linked List Example
Here we extend the struct to include both prev and next pointers, turning it into a doubly-linked list. Using DL_APPEND, we insert nodes, and then iterate forward with DL_FOREACH and backward with DL_FOREACH_REV. The ability to traverse in both directions is one of the key strengths of doubly-linked lists, and utlist.h gives you the macros to do this cleanly. Just like before, cleanup is handled with DL_FOREACH_SAFE and DL_DELETE, ensuring memory is freed properly.
// dlist.c
#include <stdio.h>
#include <stdlib.h>
#include "utlist.h"
typedef struct dnode {
int id;
struct dnode *prev, *next; // required for doubly-linked list
} dnode_t;
int main(void) {
dnode_t *list = NULL, *n, *tmp;
// ADD elements
for (int i = 10; i <= 30; i += 10) {
n = malloc(sizeof(*n));
n->id = i;
DL_APPEND(list, n); // append to doubly-linked list
}
// ITERATE FORWARD
DL_FOREACH(list, n) {
printf("forward id=%d\n", n->id);
}
// ITERATE BACKWARD
DL_FOREACH_REV(list, n) {
printf("reverse id=%d\n", n->id);
}
// DELETE ALL
DL_FOREACH_SAFE(list, n, tmp) {
DL_DELETE(list, n);
free(n);
}
return 0;
}
Sorted Insert Example
This example shows how easy it is to keep lists ordered with utlist.h. We first create an unsorted list, then call LL_SORT with a custom comparator function to sort the elements by their id. The LL_SORT macro works by rearranging the existing nodes in memory rather than copying them, so it stays efficient even with larger lists. By defining your own comparator, you can sort on any field, making utlist.h a powerful tool for building lightweight but flexible data structures.
// sortlist.c
#include <stdio.h>
#include <stdlib.h>
#include "utlist.h"
typedef struct snode {
int id;
struct snode *next;
} snode_t;
// comparator for sorting
int id_cmp(snode_t *a, snode_t *b) {
return (a->id - b->id);
}
int main(void) {
snode_t *list = NULL, *n, *tmp;
int values[] = {42, 7, 19, 5};
// INSERT sorted
for (int i = 0; i < 4; i++) {
n = malloc(sizeof(*n));
n->id = values[i];
LL_APPEND(list, n); // unsorted insert
}
// SORT list in ascending order
LL_SORT(list, id_cmp);
// ITERATE
LL_FOREACH(list, n) {
printf("sorted id=%d\n", n->id);
}
// CLEANUP
LL_FOREACH_SAFE(list, n, tmp) {
LL_DELETE(list, n);
free(n);
}
return 0;
}
Concatenation and Counting in utlist.h
Sometimes you need to merge two lists into one or find out how many elements you’re working with. With utlist.h, you can use LL_CONCAT (or DL_CONCAT for doubly-linked lists) to stitch two lists together in constant time, and LL_COUNT to get the number of items. These utilities can be incredibly handy when you’re building queues, batching operations, or managing collections that grow dynamically.
// concat_count.c
#include <stdio.h>
#include <stdlib.h>
#include "utlist.h"
typedef struct node {
int id;
struct node *next;
} node_t;
int main(void) {
node_t *list1 = NULL, *list2 = NULL, *n, *el, *tmp;
int count = 0;
// Build list1
for (int i = 1; i <= 3; i++) {
n = malloc(sizeof(*n));
n->id = i;
LL_APPEND(list1, n);
}
// Build list2
for (int i = 4; i <= 6; i++) {
n = malloc(sizeof(*n));
n->id = i;
LL_APPEND(list2, n);
}
// Concatenate: list1 + list2
LL_CONCAT(list1, list2);
// Count elements in the combined list
LL_COUNT(list1, el, count);
printf("Total elements after concat = %d\n", count);
// Iterate combined list
LL_FOREACH(list1, el) {
printf("id=%d\n", el->id);
}
// Cleanup
LL_FOREACH_SAFE(list1, el, tmp) {
LL_DELETE(list1, el);
free(el);
}
return 0;
}
Utlist Macro Reference
| Operation | Singly-Linked List (LL_) | Doubly-Linked List (DL_) | Notes |
|---|---|---|---|
| Append | LL_APPEND(head, item) | DL_APPEND(head, item) | Adds item at the end of the list. |
| Prepend | LL_PREPEND(head, item) | DL_PREPEND(head, item) | Adds item at the beginning. |
| Insert (ordered) | LL_INSERT_INORDER(head, item, cmp) | DL_INSERT_INORDER(head, item, cmp) | Inserts node in sorted order using comparator. |
| Iterate | LL_FOREACH(head, el) | DL_FOREACH(head, el) | Loop through each element (forward). |
| Iterate (rev) | (not available) | DL_FOREACH_REV(head, el) | Reverse iteration only for doubly-linked lists. |
| Safe iterate | LL_FOREACH_SAFE(head, el, tmp) | DL_FOREACH_SAFE(head, el, tmp) | Safe removal while iterating. |
| Delete one | LL_DELETE(head, item) | DL_DELETE(head, item) | Remove a specific element. |
| Sort | LL_SORT(head, cmp) | DL_SORT(head, cmp) | Sort the entire list with your comparator function. |
| Count | LL_COUNT(head, el, counter) | DL_COUNT(head, el, counter) | Count elements in the list and store in counter. |
| Concatenate | LL_CONCAT(head1, head2) | DL_CONCAT(head1, head2) | Joins two lists together in constant time. |
Best Practice
- Always include the right pointers:
nextfor singly-linked, and bothprev+nextfor doubly-linked lists. - Use safe iteration macros (
LL_FOREACH_SAFE,DL_FOREACH_SAFE) when deleting nodes while looping. - Manage your memory carefully: utlist.h doesn’t allocate or free your structs — you must
mallocandfreeexplicitly. - Keep macros consistent: don’t mix
LL_andDL_in the same struct; choose one style per list. - Prefer
*_INORDERor*_SORTwhen you need ordered data instead of rolling your own insertion logic. - Zero-initialize nodes before use to avoid undefined behavior with dangling pointers.
- Document your comparator functions if you rely on
*_SORT, so future readers (or you!) understand the ordering logic.
Conclusion
With utlist.h, you don’t just get macros for singly- and doubly-linked lists — you get a full toolkit that simplifies every aspect of list management in C. From appending and deleting nodes to sorting, concatenating, and counting, utlist.h removes the boilerplate and gives you clean, readable code without sacrificing raw performance. Instead of wasting time wrestling with pointer logic, you can focus on building faster, more reliable programs. Whether you’re implementing a simple queue, managing ordered collections, or combining multiple lists, utlist.h makes the process efficient, elegant, and headache-free.
Related
- Pointer in C – All Mysteries Revealed Now
- How to Use uthash.h to Build Blazing-Fast Hash Tables in C

Hi, I’m Cary Huang — a tech enthusiast based in Canada. I’ve spent years working with complex production systems and open-source software. Through TechBuddies.io, my team and I share practical engineering insights, curate relevant tech news, and recommend useful tools and products to help developers learn and work more effectively.





