One advantage to this approach is that there is less compiler magic going on. I use a similar approach, but I prefer using type safe upcasting or model checked downcasting via inline functions or explicit references to base members, instead of direct C style casting.
This also makes it easier to develop a uniform resource management strategy with allocator abstraction. Being able to easily switch between tuned bucket, pool, or bump allocation strategies can do wonders for optimization.
It's possible to model check that downcasting is done correctly, by adding support for performing type checks at analysis time. In this case, a type variable is added to the base type that can be compared before casting. Since this is an analysis only variable, it can be wrapped in a macro so that it is eliminated during normal compilation. Static assertions checked by the model checker during analysis time may need to be refactored to extract the type information as a proof obligation made by the caller. This technique actually works quite well using open source model checkers like CBMC.
Either way, some C OOP is not only useful to provide some optimization knobs, but it's also quite useful for introducing annotations that can help to formally verify C but that don't actually incur any runtime overhead.
The biggest advantage of C-style polymorphism vs, say, C++ is that it actually offers much better encapsulation.
Having private methods declared in the header file which is supposed to be the public contract for the class is such an anti-pattern. And the usual solutions offered for this problem are ugly in their own right (*pImpl).
I only properly learnt to appreciate the power and beauty of OOP by reading people’s C code.
Excellent writeup and straight to the point. As the author demonstrated one can get quite a lot of OOP constructs using C primitives.
what seems to be impossible to implement (as least to me) was something like interfaces, a way to decouple in a way such that high level functions don't need to know about the low level building blocks.
Um, this is how software was built in the olden days.
C++ literally began as "we could write a preprocessor to automate the tricks everyone uses to implement polymorphism in C" (CFront).
And prior to that, the same tricks were used in assembly language programming.
So this article has recreated the history of OOP, which was to create tooling to better support programming techniques already widely used. It wasn't some religion invented by the priests and sent forth on tablets, although due to humans loving them some cult, it became that eventually.
Yes, You can. The entire Linux device interface, to name just one example, if full of interfaces. The way to accomplish this is via pointer to functions, and to have it as an object, have these pointer to functions grouped in a struct.
GTK/Glib is notably full of these interfaces too.
One thing to look at is ffmpeg in its encoders and decoders. At the bottom of the file there's a struct with pointers to functions (among other things). Anything that wants to do decoding can just call init(), decode(), close() on an AVCodec and the internal functions do whatever they need to do. Here's one from h264.c:
When you read or write from a FILE in C, do you know or have to care about whether the FILE comes from disk, a pipe, a CD drive, or a device driver or a network mounted drive? What do you think FILE is if not an interface?
Hmm, I don't have much to disagree with for this link, unlike many things from that site.
One minor point - the method implementations should not be `static`, so that you can support further subclassing and reuse the base class implementations.
Note that to support both virtual and non-virtual method binding, the dispatcher also needs to be exported (with the same signature). This is already the case in the linked code but a point isn't made of it; it can be tempting to abuse `inline` but remember that is primarily about visibility [1].
It also doesn't mention how to implement `dynamic_cast` (practically mandatory for multimethod-like things), which can be quite tricky, especially in the multiple-inheritance case and/or when you don't know all the subclasses ahead of time and/or when you have classes used in across shared libraries. There are cases where you really do need multiple vtables.
Virtual inheritance, despite its uses, is probably a mistake so it's fine that it ignores that.
Is there anything you need multimethods for that can't be patched with visitors and other design patterns? They have always seemed to me like a neat feature that are devilishly tricky to implement and difficult to reason about for the average programmer.
>Object oriented programming, polymorphism in particular, is essential to nearly any large, complex software system. Without it, decoupling different system components is difficult. (Update in 2017: I no longer agree with this statement.)
The author doesn't seem to elaborate on this. I was taught OOP in university and then promptly learned that it's frowned upon in performance sensitive code, which is my main interest in programming.
(And that it apparently doesn't even achieve its stated goal of making the code easier to understand -- I've certainly had the experience of wading through a deep inheritance hierarchy (or call stack) looking for the "actual code that actually runs"...)
I'd love to hear an elaboration on that idea (OOP is essential for decoupling components) and its counterargument (decoupling can apparently be done just fine without OOP?).
Central to OOP is message passing between objects. Few languages utilize message passing, so it seems you can decouple components without OOP just fine.
> I'd love to hear an elaboration on that idea (OOP is essential for decoupling components) and its counterargument (decoupling can apparently be done just fine without OOP?).
When programmers faced the "Software Crisis" in the 1960s (https://en.wikipedia.org/wiki/Software_crisis) the idea of "Software as Components" was born as the solution (https://en.wikipedia.org/wiki/Component-based_software_engin...). This was a direct result of research on "Separation of Concerns/Information Hiding/Modularization/Structured Programming" design requirements. The idea was to make it similar to how "Components" were used in the Hardware Industry where you could substitute different components across different products/product families and all can be developed independently but used in a drop-in/plug-and-play manner to build up an entire System.
OOD/OOP turned out to be a natural architecture for Components since they provided the necessary support for the above-mentioned design requirements. There are two aspects to this architecture a) Source/Language level b) Binary/Usage level. The latter was the "holy grail" and people invented complete binary runtime architectures like COM/DCOM/CORBA/etc.(eg. https://en.wikipedia.org/wiki/Component_Object_Model) with language-neutral interfaces defined via a IDL (https://en.wikipedia.org/wiki/Interface_description_language). You could now have binary software components publish well defined interfaces which clients could call at runtime to discover and use services. The OO idea of encapsulation of data+procedures in a single "Object" keeps the model clean. But note that OO itself is merely a way of composing/structuring procedural code with a strong emphasis on a certain method of architecting the whole System.
Thus OOD/OOP helps greatly in designing systems where components can be kept well decoupled and extensible. The same can be done in a non-OO way (depending on your definition of OO) but is much harder. The design requirements mentioned above have to be satisfied whichever path you take. The role of a OO language merely facilitates the ease with which you can/cannot architect such a System.
I've found OOP can increase coupling. I actually have this problem at work.
Say I have class A and B. I could write `int x = A:FunctionOfAandB(B b)`, but in this case. So now A depends on B.
It would be much preferable to have free function of A and B. A and B are no longer dependent on each other, just the function to compute the result dependent on A and B.
IMO: The real thing of value OOP provides is calling with object.method. A lot of OOP code looks like `void obj.mutate()`. If you pulled that to what's really happening in "procedural" syntax, `mutate(obj)`, that exposes it for the bad code it is.
I've also seen template hierarchies so deep it was a major effort trying to figure out which template did anything besides forward to another template.
I had the great fortune to work briefly on the MS Word codebase, and I remember some ancient C code that manually implemented vtables. Probably not uncommon for that era.
One domain that a little OO seems to map to without too much pain is GUI libraries. The first OO-flavoured API I ever used was Sunview, the early GUI I used on Sun-3 workstations with SunOS. It was a beautiful API; I was never tempted to mess with the verbose, complex "Intrinsics-based" toolkits that followed it. It carried on with xview; that's what I'd try if I wanted to write a GUI in C today.
nanolith|2 years ago
This also makes it easier to develop a uniform resource management strategy with allocator abstraction. Being able to easily switch between tuned bucket, pool, or bump allocation strategies can do wonders for optimization.
It's possible to model check that downcasting is done correctly, by adding support for performing type checks at analysis time. In this case, a type variable is added to the base type that can be compared before casting. Since this is an analysis only variable, it can be wrapped in a macro so that it is eliminated during normal compilation. Static assertions checked by the model checker during analysis time may need to be refactored to extract the type information as a proof obligation made by the caller. This technique actually works quite well using open source model checkers like CBMC.
Either way, some C OOP is not only useful to provide some optimization knobs, but it's also quite useful for introducing annotations that can help to formally verify C but that don't actually incur any runtime overhead.
alex_smart|2 years ago
Having private methods declared in the header file which is supposed to be the public contract for the class is such an anti-pattern. And the usual solutions offered for this problem are ugly in their own right (*pImpl).
I only properly learnt to appreciate the power and beauty of OOP by reading people’s C code.
naitgacem|2 years ago
what seems to be impossible to implement (as least to me) was something like interfaces, a way to decouple in a way such that high level functions don't need to know about the low level building blocks.
dboreham|2 years ago
pjmlp|2 years ago
Regarding books, here is one from 1993, https://www.mclibre.org/descargar/docs/libros/ooc-ats.pdf
miuramxciii|2 years ago
dceddia|2 years ago
alex_smart|2 years ago
skribanto|2 years ago
o11c|2 years ago
One minor point - the method implementations should not be `static`, so that you can support further subclassing and reuse the base class implementations.
Note that to support both virtual and non-virtual method binding, the dispatcher also needs to be exported (with the same signature). This is already the case in the linked code but a point isn't made of it; it can be tempting to abuse `inline` but remember that is primarily about visibility [1].
It also doesn't mention how to implement `dynamic_cast` (practically mandatory for multimethod-like things), which can be quite tricky, especially in the multiple-inheritance case and/or when you don't know all the subclasses ahead of time and/or when you have classes used in across shared libraries. There are cases where you really do need multiple vtables.
Virtual inheritance, despite its uses, is probably a mistake so it's fine that it ignores that.
[1]: https://stackoverflow.com/a/51229603/1405588
rileyphone|2 years ago
itsboring|2 years ago
andai|2 years ago
The author doesn't seem to elaborate on this. I was taught OOP in university and then promptly learned that it's frowned upon in performance sensitive code, which is my main interest in programming.
(And that it apparently doesn't even achieve its stated goal of making the code easier to understand -- I've certainly had the experience of wading through a deep inheritance hierarchy (or call stack) looking for the "actual code that actually runs"...)
I'd love to hear an elaboration on that idea (OOP is essential for decoupling components) and its counterargument (decoupling can apparently be done just fine without OOP?).
randomdata|2 years ago
_dain_|2 years ago
https://youtu.be/QM1iUe6IofM?si=4GrOhZ31esd7rDSN&t=1089
rramadass|2 years ago
When programmers faced the "Software Crisis" in the 1960s (https://en.wikipedia.org/wiki/Software_crisis) the idea of "Software as Components" was born as the solution (https://en.wikipedia.org/wiki/Component-based_software_engin...). This was a direct result of research on "Separation of Concerns/Information Hiding/Modularization/Structured Programming" design requirements. The idea was to make it similar to how "Components" were used in the Hardware Industry where you could substitute different components across different products/product families and all can be developed independently but used in a drop-in/plug-and-play manner to build up an entire System.
OOD/OOP turned out to be a natural architecture for Components since they provided the necessary support for the above-mentioned design requirements. There are two aspects to this architecture a) Source/Language level b) Binary/Usage level. The latter was the "holy grail" and people invented complete binary runtime architectures like COM/DCOM/CORBA/etc.(eg. https://en.wikipedia.org/wiki/Component_Object_Model) with language-neutral interfaces defined via a IDL (https://en.wikipedia.org/wiki/Interface_description_language). You could now have binary software components publish well defined interfaces which clients could call at runtime to discover and use services. The OO idea of encapsulation of data+procedures in a single "Object" keeps the model clean. But note that OO itself is merely a way of composing/structuring procedural code with a strong emphasis on a certain method of architecting the whole System.
Thus OOD/OOP helps greatly in designing systems where components can be kept well decoupled and extensible. The same can be done in a non-OO way (depending on your definition of OO) but is much harder. The design requirements mentioned above have to be satisfied whichever path you take. The role of a OO language merely facilitates the ease with which you can/cannot architect such a System.
whatsakandr|2 years ago
It would be much preferable to have free function of A and B. A and B are no longer dependent on each other, just the function to compute the result dependent on A and B.
IMO: The real thing of value OOP provides is calling with object.method. A lot of OOP code looks like `void obj.mutate()`. If you pulled that to what's really happening in "procedural" syntax, `mutate(obj)`, that exposes it for the bad code it is.
WalterBright|2 years ago
tus666|2 years ago
> in performance sensitive code
Those two things are not the same.
senderista|2 years ago
pjmlp|2 years ago
1bent|2 years ago
adraenwan|2 years ago