Talk given at the Embedded Online Conference 2023, April 2023.
The standard framing of OOP — classes, inheritance, polymorphism, bundled together as a package deal — does embedded C developers a disservice. If you can’t use C++, the story usually goes, you can’t really do OOP. I wanted to push back on that.
The more useful framing: OOP encompasses encapsulation, abstraction, polymorphism, and inheritance, but the first two don’t require any class mechanism at all. They’re just well-modularized code. Once you see that, C’s toolkit turns out to be surprisingly capable.
Encapsulation and abstraction: opaque structs and singletons
Encapsulation — hiding the internal representation of a type — is straightforward in C. Declare the struct in the .c file, expose only a forward declaration in the header, and callers can hold pointers to it without being able to inspect or mutate its fields directly. This is the opaque struct pattern, and it’s exactly the kind of abstraction you’d get from a C++ class with private members.
For objects where only one instance should ever exist — a hardware peripheral, a global configuration store — a singleton with a static internal struct and a set of operating functions gives you the same interface without the allocation overhead of dynamic instances.
Polymorphism: three levels
This was the core of the talk, and I organized it around when the dispatch decision gets made:
-
Compile-time polymorphism — macros that expand differently based on type or context. Heavy-handed compared to C++ templates, but functional for things like type-generic container operations.
-
Link-time polymorphism — providing different implementations of the same function signatures in different translation units, with the linker selecting at build time. Useful for hardware abstraction layers where the “platform” implementation is chosen at build rather than runtime.
-
Runtime polymorphism via vtables — a struct of function pointers, one per “method,” stored alongside the data. The caller invokes a method through the table without knowing the concrete type behind it. This is essentially what a C++ compiler generates for virtual functions, made explicit. In C it’s most naturally applied to abstract data types (ADTs) — types with a defined interface and multiple potential implementations (a ring buffer backed by static memory vs. dynamic allocation, for example).
A note on inheritance
I closed with a brief treatment of inheritance via vtables-inside-vtables combined with magic number type tags — essentially, circumventing the C type system to embed type hierarchy information in a struct. I was honest that this is where things get complicated: you’re fighting the language at that point, the casts are manual, and the safety guarantees go away. The full discussion is a topic for another time. I mention it mostly to show that it’s possible, not to recommend it as a first reach.