andersch.dev

<2025-04-20 Sun>

Nil Struct Pointer

A nil struct is a pre-allocated struct that resides in read-only memory and represents a nil version of the type. A pointer to this struct can be returned instead of a nullpointer in case of failure.

Pointers inside the nil struct should point to other nil structs and can be self-referential (e.g. in case of a linked-list).

Returning a nil struct pointer indicates an error, but the receiving code cannot overwrite the empty defaults of the pointer. This way, the error will propagate through the same codepaths as valid data, and can be handled later on.

Example

struct Node
{
    Node *first, *last, *next, *prev, *parent;
    int v;
};

// MSVC read_only
#pragma section(".roglob", read)
#define read_only (__declspec(allocate(".roglob"))
// GCC/Clang read_only
#define read_only __attribute__((section (".text#")))

read_only Node nil_node = {&nil_node, &nil_node, &nil_node, &nil_node, &nil_node};

In the conventional approach of using null pointers, code that works on this type may look like this:

Node *SearchTreeForInterestingChain(Node *root)
{
    Node *result = 0;
    if(root)
    {
        Node *n1 = ChildFromValue(root, 1);
        if(n1)
        {
            Node *n2 = ChildFromValue(n1, 2);
            if(n2)
            {
                result = ChildFromValue(n2, 3);
            }
        }
    }
    return result;
}

With the nil struct approach, the code shrinks in complexity:

Node *SearchTreeForInterestingChain(Node *root)
{
    Node *n1 = ChildFromValue(root, 1);
    Node *n2 = ChildFromValue(n1, 2);
    Node *n3 = ChildFromValue(n2, 3);

    // we will return, and all callers can dereference this 'invalid' result
    return n3;
}

Motivation

The conventional approach to avoid access violations is by catching invalid pointers with the help of if and assert:

foo_ *foo = malloc(sizeof(foo_t));
assert(foo != 0);
init_foo(foo);

bar_t *bar = foo->bar;
if (bar)
{
    // use bar
}

This is good for genuine failure cases, i.e. failing to allocate a buffer, but the complexity grows with each if, assert and early return - doubling the codepaths in the process.

If the pointer is only being read from and a nullpointer does not indicate an allocation error, returning a nil struct pointer can reduce complexity.

Resources