I've actually started preferring the `action(subject, object)` form of programming which OO in C entails, rather than `subject.action(object)`. The latter is certainly easier for discovery via auto-complete (with most current tooling).
OO is not a natural way of programming. Everything always starts simply with functions that take arguments. Then you have a function that calls another function with some of the old arguments and some new ones.
Most people go: let's make the shared param an instance var.
The problem with a class, is that every method of the class potentially depends on every other member of the class. What usually happens is that stuff is added to the class that doesn't make sense. And every class needs a noun to name it. Then you have to think what is the proper name for this abstract thing you don't even know what it is yet. Which leads to all these quirky class names that are unnecessary.
If you are explicit about what data dependencies each function has, it becomes easier to see the commonality that should be extracted into classes. Most people just shove everything in a class too soon. And most languages push you to use classes and methods...which usually look very different to how functions are represented.
I completely agree with this. When designing classes it is very easy to get carried away and incrementally add bits to the class instead of passing dependencies explicitly.
Arguably even config is too much and generally functions should only take the subset of config.
In the end this is more art than engineering: classes allow encapsulating all state that needs to be used by all functions together; but as you start making exceptions you'll end up with the mess you describe. The art is finding the right balance between what goes in a class and what should be a standalone (possibly generic) function. Personally, I'm still far from mastering that art.
There is definitely an art to it at some point, but I see a lack of tools to be a big problem.
We are in the stone-age of programming at the moment.
I think the solution is visualization.
I want to click on any variable, and see its full data-dependency graph. All the functions it flowed through and all the transformations that occurred all the way back to a single source of truth.
With this in hand, I think it would be much easier to understand how to group methods and state. We should write code such that this data-dependency graph is easily understood by humans.
That is quite a bold claim. Can we define what is a "natural" way of programming? From my experience you are exposed to one or many programming paradigms and then, when required, you pick the one that works best for you, your problem or your team.
I still see value in modeling some particular problems with OOP and let the underlying model do the heavy-lifting. I know that under the hood every `subject.action(object)` has it corresponding `mangled_action(subject, object)`.
By the way, in our culture, the first exposure we have to functions as children is through infix operations: +, -, *, /. I would not say it is "natural". Prefix and postix operations are not "natural" either. You pick which one works best for you.
I guess I mean that it's not the first thing people reach for unless they know about it. They have some data and want to write a function to do something.
But yeh its probably a too broad statement not worth debating on definitions.
OOP has always felt to me like it was inspired by writing GUI APIs; Sun's Suntools -> Sunview -> xview (if I'm remembering the names right) were early-mid 1980s C, and felt beautifully OO, and very clean to write GUI programs. I enjoyed writing image analysis and manipulation in them. Never used the successor "intrinsics-based" Motif and CDE, the code was yucky. Didn't write another GUI until I did a dashboard (for automated software deployment) in TCL/Tk. But yeah, C++ never interested me for OO, C was fine.
It's weird that so much of OOP back then was centered around deep class inheritance hierarchies as a means to share code when my favorite more modern languages (Rust and Go) intentionally leave out this part of OOP (thankfully, imho).
An eye opener in respect to this was Sean Parent's excellent talk "Inheritance Is The Base Class of Evil" at GoingNative 2013 [1].
I really don't get why people keep repeating "class inheritance hierarchies are bad". If one understands Implementation vs. Interface inheritance and the intent behind each (the former strongly couples the code while the latter does not) then one can judiciously use inheritance techniques correctly.
The reason is implicit makes really easy to miss things and the majority of software devs have very poor understanding of coupling. Consequence is if inheritance is a thing, you'll see monsters in the code.
If inheritance is not available, the problem doesn't exist.
Same goes for immutability, in the end our tools are tuning for the lowest performers
The basic idea behind "Inheritance" is simple and can be understood by all programmers. Its realization through various language features and their combinations is what makes it somewhat confusing. Once explained and practiced with discipline, inheritance is very powerful.
Just because something is easy to understand at first does not make it suitable for designing maintainable software. I can understand the concept really well and it sounds completely appealing. When digging through the 18th code base with 12 deep inheritance hierarchies and various overwritten or not overwritten or combination thereof member functions where all you can do to follow control flow is to employ a debugger do you get the notion that this simple idea might not be so great after all.
Your comment doesn't seem to be related to "Inheritance" at all but merely a opinion drawn from your anecdotal experience and hence not generalizable. As a counter-point i have read/written my fair share of hairy OO-code (mostly C++) and i can assure you that while it was a-lot-of-work sometimes (due to the complexity of the codebase itself) it was never "throw-up-my-hands" difficult once i applied some system and method. I hardly ever use a debugger but use tools like Doxygen/Cflow/cscope/etc. to get the static view of class hierarchy/call graph and profiler/printf trace/etc. to get the runtime dynamic call graph. These give me enough insight into the architecture to become productive.
It is also a fact that OOD/OOP has been a enormous success in the real world in structuring "Frameworks" which give you a generic driver skeleton application for free into which you plugin your application specific code using (you guessed it!) "Interface/Implementation Inheritance".
Finally i recommended Bertrand Meyer's OOSC2 book because nobody explains OOD/OOP better then him in a thorough and precise manner which clears almost all of one's doubts.
I really appreciate your comment and it's absolutely true that I have no data. I don't think it's entirely anecdotal though, most newer languages and in case of JS newer paradigms are all strongly pushing against any form of inheritance, so this is an industry-wide change. I'm thinking of Go, Rust and the whole JS ecosystem that tries to write functional code (there might be others).
I wouldn't put "class inheritance" in the same ballpark as "interface implementation", one is sharing code, the other is adopting a protocol. In my statement I was referring strictly to class inheritance for the purpose of code sharing.
Unfortunately it's hard to bring up any meaningful numbers when talking about programming languages.
Define success too: OOP has been used widely, but it's way harder to find numbers representing if the codebase is easier to navigate and maintain versus this new industry trend.
Also, there is massive failure with OOP, it's just less documented than the success (look at all the companies that died due to poor software).
And yes, all I can bring is an "industry trend", but the "enormous success" you are talking about in real world is effectively an industry trend too, but of course that's been going on for way longer.
The tools you are talking about sound very interesting, but those are effectively "undoing" the inheritance, isn't that the case? The code should be written for humans, and you seem to be using a tool to literally put all the dynamic calls back into one place so that it's easier to follow.
>The tools you are talking about sound very interesting, but those are effectively "undoing" the inheritance, isn't that the case?
Oh, No!
Any system (OO or not) has two views;
1) A static view which gives the structure of the system (which is what we try to find out by browsing the codebase). Eg. class diagrams, ERDs, Sequence diagrams, State diagrams, static call graph etc. My first category of tools help you with this.
2) A dynamic view which gives the runtime call graph and state changes as the code executes. Obviously, this is harder to understand if "dynamic dispatch" of function calls are involved (which is the case in OO inheritance). My second category of tools helps you with this.
Remember, OOP is just another way of architecting Procedural/Imperative code and hence we have the same challenges (and a little extra :-)
I've been going through the book all day long, but I'm not too enthusiastic about it. It focuses so much on the "coding" that it seems to transcend the human aspect of it, that is, there will be developers that are not able to properly split classes, there will be teams, there will be incorrectly shaped boundaries, time constraint and even language limitations.
The book seems to be great at doing an incredible analysis of an OOP language, but it doesn't seem to consider the fact that the human portion of it matters a lot.
Sometimes generalizing the code so that it supports all use-cases is more expensive than just rewriting the part of the code that will change. This statement is what feels my thought as I read through the book.
And even in the analysis, it seems to focus on the ideas, but not why (and numbers) that brought to this.
I bring a simple example. I have no idea what this class does:
class Foo < Bar
end
I know exactly what this one does:
class Foo
def initialize
@bar = Bar.new
end
def something
@bar.something
end
def whatever
@bar.whatever
end
end
With the tools you described, it makes sense, you can achieve this, but without them, the problem becomes serious.
The "advantage" of the first one is that when Bar changes its public interface, Foo will change too. I don't think this is a good idea. It's nice you have to write less, but whenever a public interface changes, it should break things.
Then again, I definitely didn't write a book or have any numbers. I'll keep going through the book, but it's been quite frustrating. Very academic, so I have to skim chunks of it.
It is great that you are looking into OOSC2. Yes Meyer is somewhat difficult to read since he is so thorough, precise and detail-oriented that it can become tedious at times. Take your time going through the book to get the big picture of his OO design/implementation arguments rather than the details of the Eiffel language (not necessary during the first pass). It is a huge book so skip topics as needed to focus on the essential concepts.
You should also look into Meyer's "Design By Contract" technique for writing "correct" OO and other-paradigm programs. This is of great value in real-world programming.
> The basic idea behind "Inheritance" is simple and can be understood by all programmers.
Not just by programmers. Just about everyone says "give me something like this, but with the following changed."
That's the basic premise of inheritance and it clicks with everyone.
I think the fact that when listing languages sorted by popularity, you can draw a line in the list, and everything above that line supports traditional OO while everything below that line provides band aids for it says a lot about how much OO clicks with programmers.
It's just the latest hipster language trend. When OO was new, structural C was evil. When functional programming happened, everything before that was evil...
Because, yes, sharing state among classes for code reuse, is fundamentally evil... How dare you want to couple state and logic?
Almost like every single UI framework in every mainstream language has converged upon OO as the best way to model the problem at hand... GTk even invented a similar type system to what was mentioned in this PDF just to give C OOP constructs... Now witness as the Rust language has failed to produce a compelling UI/widgeting framework or paradigm, and the best ones out there are Qt-based or GTk-bindings (letting C hilariously handle the OOP for Rust).
>It's just the latest hipster language trend. When OO was new, structural C was evil. When functional programming happened, everything before that was evil...
Right.
By any measure OOD/OOP has been enormously successful in the real world and hence it is annoying to see blanket statements like "OOD/OOP/Inheritance/etc. is bad" on HN (i expected better). It is one thing to discuss cautions/caveats/different approaches but to dismiss the whole thing is quite silly. Hence my attempt at edification of the readers :-)
I think inheritance is the most convenient thing which came out of OOP, but interfaces are the most valuable one.
At least for business applications, you want to keep your behaviour and state separate. And people following OOP dogmatically will (ab)use encapsulation for use-cases where your data should be transparent.
Contrast this to interfaces, which are essentially composable specifications on code-level (and I absolutely love that Java uses interfaces to describe anonymous classes due to the power it gives you for documentation and specification).
The book is by the great Barbara Liskov herself (https://en.wikipedia.org/wiki/Barbara_Liskov) the pioneer of Abstract Data Types, Subtyping, OOD etc. It is basically a rewrite of their classic text Abstraction and Specification in Program Development (1986) with the example language now being Java. It is not a popular cookbook but a fundamental programming concepts book which needs serious study. I highly recommend it.
The way I'm thinking about it (just a different expression of the exact same thought) is that inheritance as a means to share code is bad, while inheritance as a means to leverage polymorphism is good. Polymorphic classes can still share code, but this should be done using composition.
Not necessarily; if they are strongly coupled related concepts within the same module then it is perfectly fine (eg. private inheritance in C++). Here you are basically structuring for code reuse and not is-a relationship.
>inheritance as a means to leverage polymorphism is good.
This is the "standard" idea of inheritance i.e. sub-typing using Liskov Substitution Principle.
> Polymorphic classes can still share code, but this should be done using composition.
Not necessarily as a combination of the above two should make clear.
OOP was never "centered around deep class inheritance hierarchies". It was centred around message passing and dynamic method dispatch. The only significance of inheritance is to resolve the receiver of a message.
I feel strongly that most of the people who think OOP was bad are people who have only ever experienced C++/C#/Java or some even weaker instantiation of OOP.
What do you suppose you're writing when you implement a (micro)service? It's nothing different than an object -- some implementation whose details you don't need to care about when you use it, which details can be changed at any time without disturbing clients (modulo idiocy), and an interface that you use to communicate with it via messages.
And don't even get me started on the OO vs. Functional nonsense...
Java is clearly influenced by Objective-C and Smalltalk in semantics.
Not only their authors are on the record saying so, Java EE is inspired in an Objective-C framework (Distributed Objects Everywhere), and Hotspot is the evolution of Strongtalk JIT.
I'm sure that Objective-C and Smalltalk have also been strong influences, especially in the libraries. But I would say that the OO semantics are very Simula like.
Can you really claim as Smalltalk inspired a language without an easy to use DoesNotUderstand? That's pretty much the basis for first class messages and transparent delegation.
edit: you could say that the JVM itself is more explicitly smalltalk-inspired as, AFAIK, has closer to fist class messaging support, but this power is hidden to the typical java program under a more interface oriented design. To reach the full power you have to escape form java into the meta-level via introspection.
The C++ like syntax was to make it more marketable.
Java, like Smalltalk and Objective-C, and others in Xerox linage, is a full stack experience, naturally one cannot dissociate the language from the JVM, that is programming with one arm tied behind the back. Language and platform were designed to work together.
As for doesNotUndertstand, for some limited use cases you would use dynamic proxies,
Agreed, I think it's been a hype at the beginning of OOP, then got overused in Java in the 90s and subsequently taught to every CS college student and it kind of stuck because in college, you don't get to maintain large software projects.
Now witness as Rust struggles to produce a compelling widgeting environment that isn't Qt or GTk bindings to other languages which support OO constructs. Witness as apparently creating a "Rust-friendly" OO paradigm or framework is an ongoing academic endeavor, because of this...
Believe it or not, for some domains, object-orientation is the right tool for the job... as some problems are just inherently modeled better with such a paradigm, and are able to take advantage of what it has to offer (encapsulation, polymorphism, inheritance).
There's a sizable chunk of people who think that the concept of inheritance for code sharing is causing more harm than good overall. These languages make it hard to do inheritance for exactly that reason.
> There's a sizable chunk of people who think that the concept of inheritance for code sharing is causing more harm than good overall.
Maybe. What I've seen online is that there is a sizable chunk of people who think that the concept of inheritance is causing more harm than good overall. I don't see many adding the qualifier "for code sharing".
> These languages make it hard to do inheritance for exactly that reason.
It makes those languages a poor fit for some problem domains, like GUIs, for example. Or games. Or many enterprise systems.
Which is why I am wondering why someone would be thankful that creating programs in certain very popular domains was made more difficult than the alternatives.
An OOP object model you can implement in C where the model is open and extensible inside the user environment. It's a very minimal yet flexible system.
This reminds me of a course I took at Stanford around 1991. The course title was UNIX C Programming and the instructor was Dr. Eric Roberts. The programming part was to build a library of simple tools and the UI was the command-line. We didn't call it object-oriented but it involved programming with ADTs that had well-defined APIs.
As far as I can tell, the author says that "ANSI-C is a full-scale object-oriented language" in a slightly sarcastic tone, as there's nothing particular to OOP languages that you can't do in C. But I've heard many people say (and mean it) they do OOP in C.
I find it a bit dubious to claim you're doing OOP in C, considering that C does not have objects. And I find it twice as dubious because you can translate any FP/OOP code to procedural C; at this rate, you can start claiming that your x86 binary is OOP.
I often hear that "FILE is an object, it's exactly as if C++ had class FILE {...}, the class is just syntactic sugar, the result of the compilation is identical".
This is false, as it's not "just syntactic sugar". A C++ class binds a function to a specific struct, and the function can "belong" to no other struct. When doing OOP you have to choose - function F belongs to either object A or to object B. It can't belong to both. Which is why OOP has endlessly silly discussions about whether class Car should Turn() the class Wheel, or should the class Wheel Turn() class Car. Or should we refactor it into class SteeringWheel that will Turn() both Car and Wheel.
This is not a thing in C. Linus Torvalds can add a function F that operates on the internals of both FILE and DIR at the same time, for example, and suddenly it's less clear who "owns" F and which one is the object.
Which is why, personally, I don't think doing dynamic dispatch with void* in C can be qualified as OOP, it's just a pattern that's been there ever since the language was introduced. It's only after you add specific OOP features to the language that it devolves into an OOP language.
I'm not sure I understand the reasoning here. It seems to me that you have it the wrong way around, so maybe I'm missing something really obvious:
> This is not a thing in C. Linus Torvalds can add a function F that operates on the internals of both FILE and DIR at the same time, for example, and suddenly it's less clear who "owns" F and which one is the object.
In this respect C is strongly-typed and you will most definitely see compilation errors if `void F(FILE *fp)` is called as `DIR *dp; F(dp)`. If you want to make a function F that operates on both `FILE` and `DIR` types, you have to bypass the type checks explicitly! IOW, it is always clear to the reader of the code that F can take either a `FILE` or a `DIR`, or only a `FILE` or only a `DIR`. There is never any ambiguity to the reader.
OTOH, with C++ ...
> A C++ class binds a function to a specific struct, and the function can "belong" to no other struct.
Unlike C above, this is never clear to the reader of the code though. For example, looping over an array of instances calling `.F()` on each instance: the reader cannot know which implementation of `F` is being invoked.
It's literally impossible to tell just by looking at the loop. Sometimes it's impossible to tell even by examining all the code because the specific method that is bound to an instance may only be determined at runtime.
for (int i = 0; i < collection_size; i++)
foo(collection[i]);
Now you say that which foo is being called is very obvious...
... but [1]:
#define foo(x) x.foo(&x)
x.foo is a function pointer and someone has been implementing OO in C (far fetched? it is literally the title of the article). With C++ at least if I M-. on foo emacs will show me all possible targets (or directly take me to foo implementation if not polymorphic); With C, good luck teaching your IDE your custom flavor of OO [2].
So, no, C and C++ are more similar than you think. The biggest difference is the presence of overloading which, in the static dispatch case, makes resolving the actual function from the function name slightly harder in C++ without tool help. Overloading has little to do with OO, although it is a form of static polymorphism.
In the dynamic case they are equivalent (well except for the fact that you have to build the vtables by hand in C and the syntax is a bit awkward.
[1] yes, yes, arguments are not protected against multiple evaluation. Sue me.
[2] with emacs of course this is actually possible, just merely hard.
To be honest I don't understand your comment, I think you're talking about something else, so I'll try to rephrase.
My point is that C is not an OOPL. There's two aspects of OOP: 1) the concrete language features 2) a pseudo-philosophical wishy-washy hippie territory where nothing is clear and everything is subjective, where you view all problems as something that is to be modelled with objects, and your own mental model is firmly centred around objects.
Since C lacks concrete OOP features, I will proceed to show why the latter aspect (philosophical OOP) is problematic.
In OOP, objects are entities composed of data and functions that operate on that data. That's what C++ classes usually are: members and methods bound syntactically together. OOP doesn't allow a method to belong to two objects, by definition and by C++ syntax.
Now, let's say you have a C API that offers 2 structs, A and B, and some functions F1A(A, ...), F1B(B, ...), and so on. The people who claim C is OOP language will say "a-ha! A is an object. The data (encapsulated too!) is in type A, and the functions F1A, F2A, ... are the methods. You could write a C++ class 'class A { private: data; public: F1A(...);...};'. Same for B. See? A and B are objects!"
This is the philosophical OOP in action, where "it looks like OOP" becomes "is OOP".
So what happens if the API adds a new function, FAB(A, B, ...), that operates on the internals of A and B? If we switch back to OOP parlance, if you had two classes, A and B, and wanted to add FAB to one of them, which class would FAB belong to?
Well, it's problematic because we're dealing with imaginary objects, so the answer can be whatever you want it to be.
You can arbitrarily say "pick the first parameter type to be the object that owns FAB". Well, what if it's FAB(const char*, A, B)? And even if A is the first one, doesn't FAB changing B break the all-precious object encapsulation? Once you introduce FAB, are these objects at all?
The problem of choosing the "owner" is a common one in C++ OOP because the objects are real (there's syntax to mark something as an object), whereas in C the objects are imaginary and, therefore, a fuzzy ambiguous concept. If you take the set of all functions whose 1st parameter is an int, does that form an object? If everything is an object, is anything an object?
But, like I said, this tends to be a non-problem in C (if you're not a OOP person) because you'll recognise that FAB(A,B) does something useful, and all this conceptual objects design is just noise that does not matter and you're happy to say "my code is not OOP but it does something useful in the best possible way".
Which is why I think that philosophical OOP (object thinking) is dubious, and the only way you can call a language OOP is if it has OOP features. Otherwise, if the only thing you're doing is applying your OOP mindset, I don't think it's fair to say that the language itself is OOP.
OOP is essentially a software architecture model. You can use this architecture in any language with a bit of discipline even if it does not give you all syntactical safety guarantees. At some point even object oriented assembler was a thing (as assembly routines may very well receive an address pointing to an object).
> OOP is essentially a software architecture model.
Finally! Somebody pointed this out.
OOP is merely a different way of structuring "Procedural/Imperative" programming. In the early days of OOP hype, the greats like Edsger Dijkstra, Niklaus Wirth were rather critical of it and denied that it was something very "Original". It was correctly pointed out as merely a technique for structuring code for "programming in the large". Once you understand this you can design and use OOP (for a specific definition) in any language.
You can absolutely translate object oriented C++ to vanilla C. As a young teenager I would routinely convert C++ from books I got from my parents/library to C, because I didn't have access to a C++ compiler. The process isn't that difficult....
Take the members of the C++ object and create a struct to hold the data then create a 'constructor' function for populating that data. Afterwards you just call the member functions of the original class, as simple C functions, and pass the struct as the first argument.
This in fact is what the actually happens at the assembly level when using a C++ compiler! The idea of an 'object' is purely an abstraction!
However, one CAN do "object-oriented" programming in C. There are ways of modeling what a piece of data is (a struct with its members). There are ways of modeling how a piece of data can behave (functions operating on those structs/pointers to structs). Perhaps this is lacking in features and expressiveness compared to OOP-first languages like C#, C++ and others, but object-oriented concepts can apply in C - look to the compiler to organize private, public functions and ownership.
I agree that you can _imagine_ that FILE and DIR are objects, you can imagine they have behaviour, you can apply your OOP mental model to procedural code.
But it all breaks down the moment you add a function that operates on the internals of both FILE and DIR, because this function doesn't belong to any one object. And if you don't notice it, you keep thinking FILE and DIR are objects, when in fact they're not (a method can't "belong" to two objects, at least as far as objects are perceived in the software world).
In short, I can agree that it's possible to write C with a OOP mindset. But (let's call it) imaginary OOP may not be actual OOP (as when enforced by the C++ compiler).
In an OO model, you can implement exactly that by having FILE and DIR derive from a common base class, then you can have your function take such a base and either act on the common subset or do a type query and cast to the derived type as needed (this is usually frowned on, but not uncommon).
But yes, of course shoving everything under OO is bad. A bit of OO should be a mean to an end, not a inflexible design principle.
> you can implement exactly that by having FILE and DIR derive from a common base class
Ah, but now you're changing the problem :) we are not in C++, we do not have base classes, we are in C and have just two so-called objects. They don't belong to us, they're exposed as part of an API.
To recap, objects are entities that bind together data and functions. You can't have a single function belong to two objects, by definition.
Some people say that FILE and DIR plus the associated functions are objects; even though there's no syntax for it, they are objects conceptually, it's supposedly the same thing.
But if FILE and DIR are 2 conceptual/imaginary objects, to which of the objects does F(FILE, DIR) belong?
And, given they're not real objects, the answer can be whatever you want it to be.
You can say that in this scenario they stop being objects at all, or you can say FILE owns the function because it's first (but then about the encapsulation, why does F use the internals of both), or you can say conceptual objects can be conjoined like siamese twins, or whatever you feel like it. You can say the set of all functions that take an int is an object. Once you go down this road, you can say anything is OOP if you look at it in a certain light.
I understand that people can/do apply the OOP mindset to anything, it's their choice. But I don't think it's fair to characterize C as OOP since it doesn't even have syntax for objects. Syntax is real, people's mental models are not. But that's just me.
Unless you are the most fervent single-dispatch OO cultist, there is no expectation that a function F(FILE, DIR) need to belong to a specific object (otherwise you could literally not have anything but unary function). And with multiple dispatch (a feature for example of CLOS), it can in practice belong to both).
FILE is definitely OO object flavored: it is an opaque handle that encapsulates state and behavior through a generic runtime-dispatched interface. The biggest issue is that it is not open to user extension (except for the nonstandard fopencookie).
As an aside, you can of course have the moral equivalent of base classes in C. See for example the POSIX sockaddr class "hierarchy".
edit: to be clear, I'm in no way claiming that OO invented this stuff. I'm sure people where building the moral equivalent of vtables before Simula. OO just gave us a language to discuss this sort of features, which is useful. It also came with a lot of philosophical baggage and design guidelines (Everything is an object!), which IMHO are less so.
I suspect that, rather than give us a language to discuss these features, OO hijacked the programmer jargon with its own terminology and heavily marketed it to become the default.
To the point where FILE is called an object, even though it's just an opaque pointer hiding the implementation. It can be phrased in OO terms but this irks me because it sounds as if OO invented it (I know you're not claiming this), and anything non-OO ("procedural, yuck") is bad. Which leads to the "everything must be an object" mindset that so many programmers exhibit today.
Which is why it bothers me to see "OOP with ANSI C". To me, those are just more or less normal patterns in C, why call it OOP when it's normal procedural code :) that's just me. Rant over.
> This is not a thing in C. Linus Torvalds can add a function F that operates on the internals of both FILE and DIR at the same time, for example, and suddenly it's less clear who "owns" F and which one is the object.
It isn’t a thing in a C++ either if one makes all the fields public or if one uses friend.
Member accessibility is not essential to the idea of OOP, since there are OOP languages which don’t have it, or in which it is merely a convention, or something which can be overridden-and a person can use an OOP language which has it yet fail to use it.
So, I don’t think “C lacks private members” is a good argument against C being “OOP”. Especially when C actually has the moral equivalent of “private” given APIs can be based on opaque pointers, so your compilation unit has no idea what the fields or layout of some structure controlled by another is.
But it is a thing in OOP. That's the point. C++ is multiparadigm. All the OOP gurus will tell you that objects shouldn't expose their internal data, only methods.
That's why the so-called encapsulation is constantly brought up. People telling you C is OOP will say "FILE is an object, the FILE struct + fopen/fclose/fwrite constitute an object, FILE is opaque, we can only interact with its internals through the public methods". "DIR is an object, ...". And so on.
And the problem is that when the Linux C API exposes a function that takes both a FILE and a DIR, and operates on both their internals, your whole idea of OOP-ness shatters because a method can only belong to a single object, by definition.
> And the problem is that when the Linux C API exposes a function that takes both a FILE and a DIR, and operates on both their internals, your whole idea of OOP-ness shatters because a method can only belong to a single object, by definition.
You can do the same in Smalltalk though-create a utility class containing a method which operates on one instance of each class, and create accessors to expose all their internals. If the ability to do that means C isn’t an OO language, then Smalltalk isn’t an OO language either. And if Smalltalk isn’t an OO language, then OO languages don’t exist.
I'm confused by the sudden digression to Smalltalk. I was talking about C. I'll restate my point.
Take the GNU C library. You don't own it, you just use it. It exposes some types (FILE, DIR), and some functions that use these types.
Then, some people look at this API and proclaim that FILE and DIR are (conceptually) objects, and that there's no difference to having 2 C++ classes, it's just syntactic sugar. And that, therefore, C is OOP (or at least the design of libc).
So now I ask - to which of the so-called objects does F(const char, FILE, DIR) belong?
The answer is whatever you want it to be, because they're only objects in your mind, and you can imagine that F belongs to FILE, or to const char, or to all of them, or FILE/DIR stop being objects, and so on. Without syntax for objects (either explicit or implicit by convention), it's up to your imagination to decide what are the objects in your program. But if imagination is the criteria for OO-ness, then any programming language can be declared OO.
OO is such a fuzzy concept that almost every feature and "pillar" is optional except objects. My point is that if a language doesn't have a syntax for objects then it's not actually OO.
You can, of course, apply an OO mindset to your C program, but you can apply an OO mindset to anything Turing-complete, from Haskell to SQL and x86 assembly, so that's not saying much. If you approach everything with an OO mindset, that doesn't mean everything is OO.
> I'm confused by the sudden digression to Smalltalk. I was talking about C. I'll restate my point.
You are arguing "C cannot be an OO language because it lets you do X, and an OO language won't let you do X." My counterargument is "Smalltalk lets you do X, but everyone agrees that Smalltalk is an OO language; hence, you claim that if a language lets you do X it can't be an OO language must be false".
> So now I ask - to which of the so-called objects does F(const char, FILE, DIR) belong? The answer is whatever you want it to be, because they're only objects in your mind, and you can imagine that F belongs to FILE, or to const char, or to all of them, or FILE/DIR stop being objects, and so on.
If a language permits methods/functions which do not belong to any class, does that mean it can't be OO?
A class-less method/function is equivalent to a single-method utility class. A language can't be OO if it lets you create a utility class?
> Without syntax for objects (either explicit or implicit by convention), it's up to your imagination to decide what are the objects in your program.
In the above program, why isn't `counter` an object? Assumably "privateData" is just a pointer to an int, but for all you know, the Counter value is actually stored as EBCDIC Roman numerals.
> My point is that if a language doesn't have a syntax for objects then it's not actually OO.
C in itself has no concept of an "object". But, there are certain conventions you can adopt in C code which make something an "object". My code above is one example of such a convention, there are others – for example, Microsoft COM (an object is a struct whose first member is a pointer to a struct of function pointers, and the remainder of the struct is private to the implementing class).
^ This, 100%. GTk's GObject type system which powers the entire GNOME stack is a similar object-oriented C type system with a virtual table pointer as the first member of an object, similar to Microsoft COM and C++.
The entire Objective-C runtime which is what powers the OO core of the language was also written with a similar purely C type system... It's just a small compiler layer on top...
> In the above program, why isn't `counter` an object?
> C in itself has no concept of an "object".
You answered your own question.
Without syntax to say "this is an object", you can take literally ANYTHING Turing-complete, mentally group data+functions and then claim "it looks like an object, therefore it is an object, why wouldn't it be an object". "Excel is OO", "Dwarf fortress is OO", "x86 assembly is OO", "pen and paper is OO".
Well, if everything is OO then nothing is OO, you've made it into a meaningless concept.
I suggest we stick to reality. The most basic requirement for a language to be OO is to have some explicit or implicit syntax for objects. That seems to reasonably partition languages into OO and non-OO.
> You are arguing "C cannot be an OO language because it lets you do X, and an OO language won't let you do X."
I'm arguing no such thing, and I never said such a thing. Would appreciate if you didn't put words in my mouth.
I'll ask again.
In my concrete example, given that FILE and DIR were classified as objects, to which of the so-called objects does F(const char*, FILE, DIR) belong? Possible answers: 1. FILE 2. DIR 3. None of them 4. Both of them 5. They stop being objects 6. New objects are created, and those own F 7.Something else
Pick one. I will then pick any other option and argue that, in my opinion, my chosen answer is the correct one. And then it's suddenly a matter of opinion rather than a matter of fact, you won't be able to prove I am wrong, just like I won't be able to prove I'm right. So much for imaginary objects.
EDIT
> In the above program, why isn't `counter` an object?
To drive my point home, why is it an object? Just because you see an object? All I see is a struct containing 5 pointers. Oh, some are function pointers? What's the difference, it's still just a struct containing 5 pointers. You consider it an object, I don't consider it an object, and none of us is wrong.
However, an instance of a C++ class containing members and methods is unambiguously an object because the syntax clearly bound them together. Plus, you can do OO things with this object, which further reinforces the idea that it is an object.
#include "oo.h"
DEFINE_CLASS(Counter,
int METHOD(Counter,getValue,NO_ARGS);,
void METHOD(Counter,increment,NO_ARGS);,
void METHOD(Counter,decrement,NO_ARGS);,
void METHOD(Counter,multiplyThenAdd,(int multiplyBy, int add));
);
That's valid C, given appropriate preprocessor macro definitions.
It is a bit ugly, due to the limitations of the C preprocessor.
(Maybe someone else can do better than me.)
If C had a more powerful preprocessor, but was otherwise unchanged, the syntax could be a lot nicer.
The DEFINE_CLASS() macro expands to declare a function:
Counter* newCounter(void);
Why isn't the return value of that function an object?
The above macros are admitttedly very hairy. If C had a better preprocessor, but was otherwise unchanged, they could look a lot nicer.
> I never said such a thing. Would appreciate if you didn't put words in my mouth.
A normal part of dialogue is, "I'm going to repeat your point back to you in my own words, and you can either agree with my restating of it, or point out at which point I've misunderstood you". That's what I was doing. "Would appreciate if you didn't put words in my mouth" is unnecessary hostility.
> In my concrete example, given that FILE and DIR were classified as objects, to which of the so-called objects does F(const char*, FILE, DIR) belong?
Take any language which you agree is "OO". Add one new feature (if it isn't already there): functions/methods which don't belong to any class/object. Now the function you are talking about is possible in that language. Did the language thereby suddenly cease to be OO when we added that feature? Most people would disagree with "Yes". But if "No", what is the actual difference between C and that language?
> > In my concrete example, given that FILE and DIR were classified as objects, to which of the so-called objects does F(const char*, FILE, DIR) belong?
> Take any language which you agree is "OO". ...
A normal part of dialogue is answering the other person's questions. I asked the same question point blank several times, and you keep refusing to answer. Once you answer the question, then we can discuss your hypothetical scenarios about other languages. I believe I stated my question quite clearly.
> Why isn't the return value of that function an object?
> In my concrete example, given that FILE and DIR were classified as objects, to which of the so-called objects does F(const char*, FILE, DIR) belong? Possible answers: 1. FILE 2. DIR 3. None of them 4. Both of them 5. They stop being objects 6. New objects are created, and those own F 7.Something else
I don't think your question necessarily has an answer, but if I have to pick, I'd say (3). F is a classless method; FILE and DIR are classes; as a classless method, F cannot belong to any class, so it can't belong to FILE or DIR.
If the function was F(FILE, const char*, DIR) then F might be a method of FILE instead, if the code base is following the "this is first argument" convention.
> I don't think your question necessarily has an answer,
And why is that? Are objects and OOP not clearly defined? If you proclaim FILE and DIR to be classes, should it not be extremely clear what are their methods?
That's the problem right there, as I said it repeatedly. In C++, objects can be identified unambiguously.
Whereas in C, if you keep insisting it's OOP, the answer is whatever you want it to be. "FILE is an object, and I'll handwave away all inconvenient details that get in the way of my perception of FILE as an object". Sorry, I expect a higher level of rigour.
> F is a classless method
> FILE and DIR are classes
> If the function was F(FILE, const char, DIR) then F might be a method of FILE instead, if the code base is following the "this is first argument" convention.
The C standard library doesn't seem to follows such a convention. For example:
> fread(void , size_t, size_t, FILE );
> fgets(char, int, FILE );
> fseek(FILE , long int, int);
If FILE is a class, is fgets a method of this class? If fgets is a method of FILE, why is F in my example not a method of FILE too?
That GTK_GRID bit is a macro that evaluates at runtime and will chuck out runtime errors if the widget doesn't match. It supports inheritance. If you have for example a gtk_combo_box_text, you can use gtk_combo_box_set_active(GTK_COMBO_BOX(my_combo_box_text), 0) and it will select the first entry in the gtk_combo_box_text because gtk_combo_box_text inherits from gtk_combo_box like any good object oriented system.
Motif/libxm is very similar to that. With two important exceptions: the user code does not use type-checking macros and “stringly-typed” API for set a property to something is more or less a part of the overall ecosystem (well, Xm is just an layer built on top of Xt).
Gtk+ was originally an implementation of whatever parts of Motif were used by Gimp, built on an underlying object model that has nothing to do with Xt (which has both good and bad implications).
I'm a huge academic fan of what GTk has accomplished (thanks to their GObject type system), and as much as I know the C89 crowd who thinks macros are all evil probably abhor this kind of thing, I think GTk is one of the most epic, impressive, ambitious C codebases in existence. Witness as non-OO C matches, rivals, and quite often beats Qt on equivalent classes/features in plain C... It's even above "just C++ style C," as they have added features like a property and signal system...
I'm such a fanboy it inspired my own type system and massive core library, libgimbal, which uses a type system similar to GObject and targets game consoles like the Sega Dreamcast: https://github.com/gyrovorbis/libgimbal.
I remember writing a GTK extension widget many years ago, and yes, it was very much like this. The macros end up making your very own hand-coded vtable-equivalent, so that function calls get dispatched to the appropriate point in the inheritance chain. It was pretty eye-opening!
Exactly. Amen. GTk is the shining proudest most epic achievement of object-oriented C programming, and they still enjoy massive benefits in terms of being able to interop with every language ever because of this approach.
The Linux kernel contains a lot of C OOP, similar to GTK and other such frameworks.
As soon as you need bindings done during runtime, you will end up with structs containing function pointers as some sort of interface descriptor, and potentially that is embedded or aligned with structs containing data. This is at least OOP class level functions with data they can operate on and if you allocate the "ops" struct (together with data) as well we are talking actual object instances. The Linux kernel is full of such things, plus there are even macros to cast up/down the inheritance tree.
One thing that's bothered me for a few years about Object-Oriented code, is the question of why the dichotomy of 'implementation' vs 'interface' is drawn on the exact same line as 'variables' and 'functions'.
Everyone who's ever worked in a functional programming languages knows that functions can easily be considered 'state', and everybody who's spent time in real-life plugging in various electronic devices knows that data itself can be considered an 'interface'.
So why does OOP dogma conflate these ideas together? Why couldn't a single variable or property be virtual and polymorphic? Why couldn't a member method be reassignable like a member variable? The philosophy and the actual implementations just self-reference each other. Interfaces are great because they keep interfaces separate from state. I agree. But strictly in terms of language features, why are functions considered interface while variables are considered state? Well, because that's just how they're defined, I guess.
But if you're doing OOP in C, and you have a VTABLE full of pointers, well, it doesn't really matter what those pointers point do, do they? They could be function pointers, they could be variables/memory locations, they could even be references to other structs. It's not quite as delineated when you do it from the bottom up.
Not quite sure what you are trying to say here; but ...
> the dichotomy of 'implementation' vs 'interface' is drawn on the exact same line as 'variables' and 'functions'.
That's not true; They are orthogonal to each other.
OOP is just a way of structuring Procedural/Imperative programming; "variables" maintain state while "functions" change/read state. You are just bundling them together to gain fine-grained modularity. An "interface" is anything (i.e. can be both variables and functions) which is "publicly" accessible by a client. An "implementation" is conversely not accessible by the client (i.e. encapsulated) but can also consist of "variables" and "functions".
> Why couldn't a single variable or property be virtual and polymorphic?
Not sure what you mean here but you do have tagged unions/variants/sum types which gives you what you maybe looking for.
> Why couldn't a member method be reassignable like a member variable?
It is possible in certain object models. Lookup "Metaclasses" in your favourite language for possible support. If you want to see example code in C++, checkout the classic Advanced C++ Programming Styles and Idioms by James Coplien. The book contains lots of examples of mind-bending OO trickery using C++.
This book is an absolute classic. I came from C++20, using concepts and constraints, doing a lot of compile-time metaprogramming and shenanigans, and I set off on a quest to rewrite a lot of my core tech stack in plain ol' C ('17 revision) for language interoperability reasons...
...and I literally almost gave up on being a C programmer because of bashing my head against language limitations... This book completely and totally changed everything and is the reason I completed this very ambitious project (https://github.com/gyrovorbis/libgimbal)... It's not just good for object-oriented C, but also for approaching the C language differently in general. For pushing its boundaries rather than accepting its limitations.
Anyway, this "PDF" was such an inspiration on me as a C programmer that I managed to track down a physical copy on Ebay. lol.
Can't find the link now, but there was some malware recently (maybe under 3 years ago) that was found to directly use this book as a basis for their code.
I skimmed the first couple of chapters and the syntax you have to go through to make this work looks painful at best. void * everywhere. Structs with C's function pointer syntax all over the place... It looks like it can certainly work, but it's not going to be pleasant compared to almost anything designed for OO.
You don’t need all that to emulate objects in C. Instead use this recipe:
- classes <-> compilation units
- objects <-> structs
- methods <-> functions receiving a pointer to a struct as first argument; use name of the class as prefix e.g. Point3D_scale(Point3D *self, double factor)
- constructors are just methods and initialize the struct
- destructors are just methods and free all memory occupied by the struct
Inheritance is a bit more difficult this way but as others pointed out already it is way over used IMHO. But if you really want to do that you could design a child class by using a struct pointing to the parent struct and using that to manually delegate all inherited method calls to the parent. You could probably write a fancy macro automating this delegation for you.
I did “false object” programming with C, back in the mid-90s. I never read this book, but we came up with similar approaches. I got the idea from QuickDraw GX. I passed around structs, as “nouns,” and some had built-in “verbs.”
I used function pointers, and these created kind of a “poor man’s vtable.” Not exactly “polymorphism,” but similar.
I needed to do it for a modular imaging peripheral SDK. Back then, the only reliable way to cross an external module boundary was with ANSI C (stack conventions).
It worked quite well, as they were still using the same SDK, 25 years later.
You’re right about the syntax becoming extremely tedious. Eventually you wind up doing waaaay too many function pointer casts and typedefs.
The appeal of this approach, to me, is mostly pedagogic rather than practical: stuff designed for OOP isn’t generally going to let you control struct layout to the degree that C will, making it harder to get your hands dirty and learn core concepts.
I read this whole book maybe 15 years ago. I don’t remember much detail at this point and the project I was working on at the time has been retired. Still, I have fond memories of the learning process. Manually building up single inheritance OOP with structs and vtables using C is very educational.
I suspect the state of the compilers in 1993. And maybe the standard even. When I was using C++ a lot in 2000 it was difficult to get non-trivial code working across compilers.
certainly true (first c++ standard was in 98) but i think this is more a library issue than anything else, which isn't germain to this article. i haven't read the article in depth, but things like new/delete, constructors and virtual functions in wide use in 93. and let's not forget that C itself was only standardised in 89, and it took vendors a long time to catch up with that standard.
Library issue was my understanding - the Half-Life mod Natural Selection used the STL heavily and was built for both Windows and Linux (Linux for dedicated servers).
When the source code was released many years later the documentation said it needed a library called stlport - some third-party implementation of the STL because even in 2002 when Natural Selection was released, different compilers apparently shipped STLs of varying quality.
STLport was the open sourcing of SGI's STL implementation, and its existence I think kinda predated the completion of a full STL shipped with either MSVC or GCC. It got used back then because of, yeah, varying quality between compilers but also varying consistency. If you wanted something that could compile on both G++ and MSVC7 or earlier you were probably going to reach for stuff like that, or have a pile of #ifdefs and pain.
C++ is a far nicer language to work with these days. Back then everyone rolled their own string etc. library, too. Sucked.
i don't know about the half life stuff. stlport was (is?) a portable implemrntation of the Standard Template Library. this is far from the same as the C++ Standard Library.
It was only used by one of the bigger Half-Life mods (nothing first party - Half-Life's game code is very much C with classes). Possibly other mods used it but I suspect that most Half-Life mods kept to the style of the original.
You'll do object-oriented programming with C in 2023 if you want to use the main official API of the GStreamer multimedia framework [1], which is C-based and heavily using GObject object model [2], which itself is part of GLib (the underlying library of GTK)
This. The entire GNOME stack is based on a steroided-out C-based OO type system in the same vein as the OP's PDF... and it's actually how it gets bindings to every language ever so easily...
Anyway, I was sent down a rabbit hole of wanting to do object-oriented C for a language independent core as well, starting off with buying the physical copy of this PDF, because I liked it so much...
I have to say GStreamer is probably the absolute best, most ambitious, shining example of such a type system, as it's already built upon GObject, fixes a lot of its shortcomings, and just really takes the whole thing to the next level with a complex, highly OO architecture. Fantastic reference and inspiration for this kind of stuff.
There are a few niche reasons. The entire GNOME/GTk stack benefits greatly in the realm of language interop from being C at its core. This is why it easily has bindings to every language ever.
I'm the author of the libGimbal project linked to there. It's a massive object-oriented C codebase with an entire cross-language type system that was inspired by GTk's GObject, Microsoft COM, and the Objective-C language.
I've studied everything out there on achieving OOP in C, and actually the OP's book reference was my original inspiration. There's a fine art to doing it and a few good codebases are out there that do it well... The entire GNOME stack is based on this kind of stuff.
Anyway let me know if you have any questions. lol.
Hey dude, awesome work and thanks! I'm mostly interested in a curious kind of way, I really enjoy programming procedurally mostly but sometimes being able to utilize OOP for certain things is very nice.
Really in the end I should just make my own language but who has the time right.
OO is not a natural way of programming. Everything always starts simply with functions that take arguments. Then you have a function that calls another function with some of the old arguments and some new ones.
Most people go: let's make the shared param an instance var.
becomes... The problem with a class, is that every method of the class potentially depends on every other member of the class. What usually happens is that stuff is added to the class that doesn't make sense. And every class needs a noun to name it. Then you have to think what is the proper name for this abstract thing you don't even know what it is yet. Which leads to all these quirky class names that are unnecessary.If you are explicit about what data dependencies each function has, it becomes easier to see the commonality that should be extracted into classes. Most people just shove everything in a class too soon. And most languages push you to use classes and methods...which usually look very different to how functions are represented.