The Singleton Pattern in C
Singletons get a bad reputation. In object-oriented languages, they’re often a code smell — global mutable state hiding behind a class. But in C, with the right library support, they solve a real problem elegantly.
The Registry Problem
Imagine you have a system where multiple modules need to look up definitions by ID. Card effects, status effects, templates — all defined once, referenced everywhere. You could pass the registry through every function call, threading it through layers that don’t care about it. Or you could use a singleton.
How It Works in Practice
The pattern is simple: a static pointer, initialized on first access, never freed.
static MyRegistry *default_instance = NULL;
MyRegistry *
my_registry_get_default (void)
{
if (default_instance == NULL)
default_instance = my_registry_new ();
return default_instance;
}
Callers get the registry with a single call. No threading a pointer through six layers of function calls that don’t use it. No global variable that anyone can reassign.
Why It’s Not the Same Problem
The OOP critique of singletons is about hidden dependencies and testability. But in C, the tradeoffs are different:
- No inheritance — you can’t subclass a singleton into something unexpected
- Explicit initialization — the
_get_default()pattern makes the first-use cost visible - Read-heavy usage — registries are populated once and queried many times; mutation isn’t the concern
- No DI framework — C doesn’t have dependency injection containers, so the alternative is worse
When It Breaks Down
Singletons in C go wrong when they hold mutable state that changes during operation. A registry of definitions is fine. A singleton that tracks “current combat state” is not — that’s genuinely global mutable state, and it will bite you in concurrent scenarios.
The rule is simple: if your singleton is a phone book, it’s fine. If it’s a notebook, reconsider.
The Testing Angle
For tests, you can add a _set_default() or _reset() function that lets test fixtures inject a known registry state. This preserves testability without the production-code complexity of passing registries everywhere.
The pattern earns its keep when the alternative is worse. In C, it usually is.