Object-Oriented C: A Primer
By Artyom BologovC is a marvelous language in how it can be small, efficient, pretty, and ugly, all at the same time. But what it lacks (driving modern developers to despair) is Object-Oriented Programming (OOP) facilities. Yes, there's C++, but one doesn't talk about C++ in good social circles. Especially when it's possible to simulate classes, objects, and methods in C. Yes, it'll look slightly irregular, but bear with me.
This post is structured around Object-Oriented Programming concepts. Starting from the simplest ones and slowly increasing the difficulty level. Be prepared: OOP is quite a fuzzy set of ideas. I'll exploit this fuzziness as much as possible—to stay within what people call OOP while not parting with C and its blessed ways. As a spoiler: the resulting system will be a Generic-based Single Inheritance one.
Here's a problem domain we're going to model: animals. There are animals (yes, a classic OOP example). Many families and species of animals. I have a lovely cat named Kalyam, so I'm mostly interested in Feline.
I'm going to model just that part of biological hierarchy. Hopefully, we'll have enough material to crash-test the approach I suggest.
Encapsulation
This one is easy. In its simplest definition, encapsulation is putting things into their buckets (called classes). Encapsulation might also mean hiding data inside classes, but see visibility section for that instead. Encapsulation might also mean belonging of behavior (methods) inside classes. But that's debatable: some languages have a generic-oriented OOP where methods are freestanding entities. If anything, methods belong to their generic functions there. That's the approach I'll use. C (since C11) has generic dispatch, so why not use the feature that's already there?
Putting the generics talk aside for a second, let's encapsulate some data, shall we?
That's it, we've got our parent class ready. The data is contained within, so we've got encapsulation. What we don't yet have are animal classes, orders, genera, species, etc. So let's create an extinct animal that can forgive me my poor biology knowledge:
In case you don't like this
struct
talk, you can always define a macro and a type alias (capitalized, to please the Java crowd):
That's all there is to encapsulation, really.
Inheritance
With encapsulation out of the way, enter inheritance. A way for classes to depend on each other and share behaviors/data.
A trick I learned (from the "Good Taste" series of posts) just a few days ago: embedding structures inside each other. Putting one structure as the first member of another, you're making the outer structure castable to the inner one, sharing the behavior between the two:
Now we can cast any animal to (Animal *)
and invoke Animal
methods:
No, wait our animals don't know how to eat yet! Let's teach them—with polymorphism!
Polymorphism
There's a default behavior to animals: they eat (duh).
That's why the diagram above includes the
eats()
method: any animal class eats something.
However, there are all sorts of animals.
Some eat plants.
Some eat fungus.
Some eat other animals (huh, a recursion?)
Knowing that the creature is an animal, it's hard to tell what they eat.
Here's how we express it with code:
For now, our eats()
macro/generic only has one default method: animal_eat
.
But you can already see how one can extend it with just another line of type+method.
Let's actually do that:
Only one more line in the generic, and we have carnivoire-specific behavior! That's what polymorphism's promise is: specifying behavior given a type.
Visibility
Most OOP systems have private/public/protected differentiation. I can easily cast it aside based on the fact that e.g. Python doesn't have visibility as a concept. But I'll try to implement it anyway.
The trick is treating structures as opaque data.
I mean, the user of the code doesn't have to know the data layout of the structure.
They have to use it as a raw pointer, relying on extern
-ed functions anyway.
This is exploited by many codebases.
They tend to hide the pointer to the "private" version of the structure, nested inside the "public" one.
WebKitGTK does this:
Relying on this tradition, we can say that structures are private by default. What's public is their getters and setters. So why not define some getters and setters for our structures?
This is pretty boring, so let's define a new class with private fields and methods:
Notice that we don't have a setter for
claws_out
—retract/protract methods handle modification.
An important OOP technique of hiding the actual data behind the behavior.
Using the OOP system
The code so far was pretty simplistic, and there wasn't much OOP. This section and example will finally put the system to the test. Let's define cats (I've been waiting for this!) and their behaviors:
Testing this system yields:
These nested curly brackets are not looking right, begging for constructor methods. But this post is too long already, so let's leave it for later. What's important: Encapsulation, Inheritance, Polymorphism, and Visibility are there. C can do OOP. And it's not that hard really—what this post covers is quite simple and easy to scale.
You can look at the final code (compiles with GCC and Clang, even if with heaps of warnings) in oop-c-primer-original.c. And the cleaned-up (and slightly extended with bird-specific details if you wanted to see a more involved inheritance hierarchy) code in oop-c-primer-cleanup.c. I hope that you're convinced OOP is possible in C, for better or worse. Thanks for accompanying me on this journey!
Acknowledgements
I owe a thanks to- Vasily Gerasimov for our discussions on what is or isn't OOP.
- Alex Alejandre for being attentive to my language and curious about the idea of the post.
- Post-Modern C style guile for making me even consider a possibility of OOP in C.
- The Extendible _Generic post from CC for all too late a realization of a better approach. They did OOP better, let's agree on that.