**OOP in C as a poor man's vtable dispatch — explained in deliberately harsh category-theoretic language**
We begin with the only honest definition of “OOP in C” that does not lie to itself:
**Definition (OOP-in-C style)**
An “object” is a struct that contains (among other data) at least one function pointer (or more typically a pointer to a table of function pointers — the vtable).
Every “method” is an ordinary C function whose first (and usually only meaningful) parameter is a pointer to the struct instance itself:
```c
struct Shape {
struct ShapeVTable const *vtable;
float x, y;
// possibly more payload fields
};
typedef void (*ShapeDrawFn)(struct Shape const *self);
typedef float (*ShapeAreaFn)(struct Shape const *self);
struct ShapeVTable {
ShapeDrawFn draw;
ShapeAreaFn area;
// …
};
```
This is the *only* mechanistically truthful way to write anything resembling classical single-dispatch OOP in pure C.
### The central collapse — the machine has only one kind of “object”
No matter how many source-level “classes” you write
```c
struct Circle { struct Shape base; float r; };
struct Rectangle { struct Shape base; float w, h; };
struct Triangle { struct Shape base; float a, b, c; };
```
the generated code — after inlining, constant propagation, and especially after monomorphization / devirtualization opportunities are considered — very frequently reduces to **exactly one memory layout family** with different vtable pointers.
Even when the layouts are genuinely different, the *dispatch mechanism itself* lives in one universal carrier category whose objects are essentially
- memory regions beginning with a `void *` or `uintptr_t` that points at a table of function pointers
- or memory regions beginning with a single function pointer (single-method case)
In other words, the **carrier** of the “object” category you imagine in your head is almost always a very thin slice of the actual category of C types at runtime:
**Hom(C⁰ⁿᵗᵉˣᵗ, • → •)**
where • is the forgetful functor that throws away everything except “starts with a function pointer or vtable pointer”.
### “Inheritance” = replacement = forced sectioning = very bad section functor
When you write
```c
struct Derived {
struct Base base;
int more_stuff;
};
```
you are telling the compiler:
> Please pretend that the initial segment of every `Derived` is a `Base`.
This is **not** an embedding functor
This is **not** an inclusion of categories
This is a **replacement declaration** followed by forced pointer coercion.
In category-theoretic terms you are doing something morally equivalent to:
Let `U : C → Set` be the forgetful functor from your pretend “class category” to sets-of-bytes-with-layout.
You then force
U(Derived) ≅ U(Base) ⊕ ℤ × … × ℤ
but simultaneously you demand that every morphism that was originally typed
f : Base → Base
can be postcomposed with the section
s : Base ↪ Derived
and precomposed with the retraction
r : Derived → Base
such that
r ∘ s = idBase
but in **almost all real compilers** the section s is the identity on the byte level (zero-cost upcast) and the retraction r is also frequently the identity (zero-cost downcast in the prefix-layout convention).
So the diagram
s
Base ⇄ ————→ Derived
r
collapses to the trivial groupoid on one object in the compiled code in the vast majority of call sites.
This is why “extends” is a homonym for “replace prefix with a wider prefix and pretend the narrower prefix is still meaningful”.
### The fatal wound: there is no faithful functor from “OOP code space” to “machine space”
Let us try to be painfully precise.
Define two categories (grossly simplified):
**Code-space category 𝒞 (the fantasy the programmer is sold)**
- objects = named classes (Animal, Dog, GoldenRetriever, …)
- morphisms = method tables + implicit upcasts (covariant in the “self” argument)
**Machine-space category ℳ (what actually runs)**
- objects ≈ {memory regions that begin with a vtable pointer or fat pointer}
- morphisms ≈ {indirect calls through a function pointer at a known offset}
Now ask: does there exist a functor
F : 𝒞 → ℳ
that
1. preserves the “is-a” structure on the nose (i.e. is faithful on objects and morphisms),
2. preserves composition of method calls,
3. preserves identity (every object can call its own methods without change)?
The answer is **no** — for several independent reasons:
**Reason 1. Object collapse (surjective but not injective)**
Many distinct objects of 𝒞 are sent to isomorphic (or even identical) objects of ℳ.
**Reason 2. Morphism collapse**
A very large set of morphisms in 𝒞 (different method names, different static types) are sent to the exact same indirect call instruction differing only in the constant vtable index.
**Reason 3. No inverse up to natural isomorphism**
There is no faithful “downcast” functor going the other way that would recover the original rich type from the flattened memory region. The forgetful functor is not essentially injective.
**Reason 4. The vtable itself destroys cartesian closure**
Even if you squint and say “the vtable is the image of the Hom-set”, the image of the entire Hom-set under F is a single function pointer table — which is a set, not a category. You lose the higher-dimensional structure.
In short: **there is no faithful, structure-preserving functor from the advertised OOP category to the runtime category.**
What actually exists is a **forgetful, many-to-one, non-invertible, non-faithful projection** that discards almost all the structure the programmer was told existed.
### Summary verdict in category-theoretic polemic language
OOP in C (and in most C++-style implementations) is:
- a manually maintained vtable-indexed endomorphism monoid
- wrapped in a syntactic fiction of rich “classes” and “inheritance”
- whose promised categorical structure is systematically destroyed by a forgetful, surjective, non-faithful collapse
- that forces “extends” to be read as “replace-with-prefix-and-forget-you-did-so”
The moment you accept that the machine only ever sees one (or a very small number of) memory-layout shapes plus an integer index into a static table of function pointers, the entire edifice of classical single-dispatch OOP starts to look like an expensive, brittle, non-compositional cosplay of procedural programming.
Everything interesting that people reach for (visitor, double dispatch, CRTP, typeclasses, protocols, etc.) is either
- an attempt to recover some lost categorical structure after the compiler has already thrown it away, or
- an admission that the original “OOP” category was never faithfully representable in the first place.
That is the tragedy in one sentence:
**The category the programmer believes in does not survive translation — and the compiler knows it, but the marketing department never told the programmer.**