Skip to content
Home » All Posts » How to Use utlist.h to Craft Ultra-Swift and Elegant Linked Lists in C

How to Use utlist.h to Craft Ultra-Swift and Elegant Linked Lists in C

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

OperationSingly-Linked List (LL_)Doubly-Linked List (DL_)Notes
AppendLL_APPEND(head, item)DL_APPEND(head, item)Adds item at the end of the list.
PrependLL_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.
IterateLL_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 iterateLL_FOREACH_SAFE(head, el, tmp)DL_FOREACH_SAFE(head, el, tmp)Safe removal while iterating.
Delete oneLL_DELETE(head, item)DL_DELETE(head, item)Remove a specific element.
SortLL_SORT(head, cmp)DL_SORT(head, cmp)Sort the entire list with your comparator function.
CountLL_COUNT(head, el, counter)DL_COUNT(head, el, counter)Count elements in the list and store in counter.
ConcatenateLL_CONCAT(head1, head2)DL_CONCAT(head1, head2)Joins two lists together in constant time.

Best Practice

  • Always include the right pointers: next for singly-linked, and both prev + next for 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 malloc and free explicitly.
  • Keep macros consistent: don’t mix LL_ and DL_ in the same struct; choose one style per list.
  • Prefer *_INORDER or *_SORT when 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

Tags:

Join the conversation

Your email address will not be published. Required fields are marked *