Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
OOP in C (washington.edu)
185 points by philonoist on Jan 8, 2023 | hide | past | favorite | 135 comments


A whole book on this topic was already written in 1993 by Axel-Tobias Schreiner. It seems to be freely available nowadays:

https://www.cs.rit.edu/~ats/books/ooc.pdf

(edit: link updated to point to author's website)


I started reading rhis last year in my free time, and following along coding myself.

I only read a few chapters then got sidetracked, but man I learnt so much stuff from reading just a bit

i already knew all the C concepts he was using but it never occurred to me to use them in that way.

The book is very well-written and easy to understand, and also very approachable even for someone who is not so experienced with C, like myself

I want to start over someday


That was how I was doing OOP in C in the late 80s and early 90s. I found many of those concepts both in Python and in JavaScript. After all they were born around that time.

Python because of the insistence of declaring that explicit self argument, the pointer to the struct for the object.

JavaScript because of the prototype based OO. You can change the meaning of any field in an OO C struct if you know how to handle it later.


Years ago, I started an implementation of that book: https://github.com/linkdd/ooduck

Never truly finished it, but it works well :)


Fyi, fwiw, the top-of-page "About Duck-Typing C library based on ooc.pdf linkdd.github.com/ooduck/ " link is currently 404.


Ah thank you, it predates Github's renaming of Github Pages from .com to .io --> https://linkdd.github.io/ooduck/


The webpage in the title is pretty good. It explains most things you need to achieve OOP in C. This PDF however pushes C for what it is not intended for, which leads to inefficient and obscure code.

For example, the first example in the PDF implements a Set. Everything is "void". This loses typing and will make code hard to read and maintain. The way inheritance is mimicked is hideous and inefficient. The PDF also heavily uses function pointers, which can be slow (because they aren't easily inlined) and waste memory (because each pointer costs 8 bytes on x64).

In all, most large C projects are doing OOP at some level. However, C is not C++ and is not intended for providing all OOP features like inheritance. This book should probably be titled "Wrong ways to OOP in C".

PS: The book does have some interesting ideas, but those are the "clever" things you will have to unlearn later. The author provides the source code for the book [1]. Have a look at the string implementation in Chapter Two. That is the most arcane and inefficient string implementation I have seen.

[1] https://www.cs.rit.edu/~ats/books/ooc-14.01.03.tar.gz


I have not read the book.

> However, C is not C++ and is not intended for providing all OOP features like inheritance.

There are parts of the ISO C standard specifically included to allow for inheritance. Consider:

typedef struct{ int x; }struct_a;

typedef struct{ struct_a inherited; int y; }struct_b;

void function(struct_a *s) { s->x = 0; }

Here struct_b inharates struct_a. You can call the function with either a pointer to struct_a or struct_b (either by casing or void pointer), because the standard specifically states that there can be no padding before the first member of a structure, so that the first member of a structure should have the same pointer as the structure itself. This part of the spec was written with the use case of inheritance in mind. (there is some people who don't read the spec that way, but this is the intention)

Sorry for being nitpicky... but that's pretty much what we do in the wg14....


Doesn't this violate strict aliasing? E.g. what if we do this?

    void foo(struct_a *a, struct_b *b) { a->x = 0; b->x += 1; a->x = 0; }
Strict aliasing allows the compiler to elide the second a->x = 0. But if we use your inheritance scheme using casting of pointers, we could have a == b.


This book is a really fun read. You’ll learn a lot and get some interesting ideas.

Don’t listen to prescriptions from strangers on HN, how about? Myself included. :-)


I remember having a book on the shelf through the 90s entitled 'Object Oriented Programming in Macro Assembler'



This sounds like a nightmare


I wasn't too keen on this book. I read it hoping it would be more like the article in this post. But I felt it was more a case of someone re-inventing c++ with macros.


You mean "C with classes" ?


nope, it's this book: https://www.goodreads.com/book/show/30040029-object-oriented...

i was basically reiterating my comment from there.


C required the simplest form of OOP, but who standardizes it decided to deny the feature to the language: the ability to have callable structure methods (function pointers) with explicit "self" pointer. Not even constructors/destructors. After all it's C, and you can write list->init() and list->free(). This simple form to bind data and the functions operating on such data would make many codebases better.


I made a pre-processor [1] to add similar features to C, and after reading your comment I’m thinking that it would be simple to add a setting/#pragma to do these transformations:

    list->init(); → list->init(list);
    list.init();  → list.init(&list);
[1] https://sentido-labs.com/en/library/cedro/202106171400/#back...

I normally avoid the function pointer overhead, which can be done with _Generic:

    #define append(VEC, START, END) _Generic((VEC), \
      Vec_float*: append_Vec_float, \
      Vec_str*: append_Vec_str, \
      Vec_cstr*: append_Vec_cstr \
      )(VEC, START, END)
https://sentido-labs.com/en/library/cedro/202106171400/#loop...

But list->init(list) might be a simpler solution for most cases, and compatible with C89/C99.


I’ve built it for now in a separate branch called “self”:

    git clone -b self https://github.com/Sentido-Labs/cedro.git
    cd cedro
    make bin/cedro # Just “make” will build cedrocc etc.
    bin/cedro - <<'    EOF' # Mind the indentation.
    #pragma Cedro 1.0 self
    list->init();
    list.init();
    list->append(123);
    list.append(123);
    EOF
The “self” flag after “#pragma Cedro 1.0” activates the “self” macro, because it should not be done by default.

Result:

    list->init(list);
    list.init(&list);
    list->append(list, 123);
    list.append(&list, 123);
I’ll try it out for a few days and if it works well in practice I’ll document it and merge it into master.


> I made a pre-processor

You followed the path of C++.


> You followed the path of C++.

To some extent yes, I know about cfront.

This is just another iteration on that old idea.


Sure; you’re basically implementing The prehistoric version of C++, called “C with Classes.” If you’re unaware, see Stroustrup’s Design and Evolution of C++ book.

Which is fine! Should be interesting.


That wouldn't actually help much. In the case that there is no inheritance, using a function pointer has unnecessary overhead (pointer storage, indirect call, additional parameter). In the case of inheritance, the pointer type differs and thus the function pointer type wouldn't be compatible either.


You need to use a void* self pointer. And indirect through the vtable in all calls to virtual methods, even those that are part of the object itself. Thus, e.g. a method defined on a base class might end up calling an implementation deep in the inheritance hierarchy, that didn't even exist when the base class code was written. There's no real equivalent to this when doing simple interface inheritance, but it's very much part of the actual semantics here.


Inheritance is nowadays almost considered a bad practice, in fact newer languages, such as Rust, doesn't support it, and in languages that has it (Java, C#, etc) nowadays they always tell you to prefer composition over inheritance.

> using a function pointer has unnecessary overhead

This is true, it's inefficient. But for a lot of application, also irrelevant at performance level, and would provide a good abstraction.


Inheritance is the best solution to some problems.

It got a bad reputation because it was abused. Bad practice is using inheritance when a tuple or a map would suffice.


To some problems that are almost always seen in the academic world but I've yet to see in my daily job.

Inheritance (from implementation, I've nothing against implementation of interfaces or inheritance from abstract base classes) has a ton of problems, more importantly the fact that it makes the code more difficult to understand and to evolve.

Composition on the other hand is something more natural, even if we think about real life: you don't usually take an object and "extend" it, you take multiple object and use them together to build something!


It's strange that you seem to be an absolutist on this topic. What is wrong if inheritance is an elegant solution to a relatively small number of problems? Historical overuse of inheritance doesn't take away from that.


What problem is left? UI? React is showing composition wins there too.


It's just a code reuse mechanism. In some cases you can do with it what you might also do with callbacks, or yes, composition. Or in some cases inheritance might be handy.

I don't know why we need to be so judgmental about it.

I think inheritance is especially good if you have an interface (in the OO sense, like some languages use an "interface" keyword for), but you have some common or default methods, which a specific implementation may or may not override, or maybe there is some boiler plate or tedium where the most common implementation might belong in a base class. I think this is handy for something like a device driver.


  > has a ton of problems
this may be a good reference with examples (and some good dose of nuance) on the subject:

https://blog.gougousis.net/oop-inheritance-what-when-and-why...


The main issue with inheritance is hard coupling of data and code. As code evolve from data modeling point of view it can be advantages to split the state managed by one object into several data structures. With data inheritance such refactoring is almost impossible. When inheritance is limited only to pure interfaces the access to the state is not hard-coded and can be redirected as necessary.


AIUI you can implement inheritance as a pattern on top of existential types, using a self-recursive definition trick. Not sure if Rust has full existential types yet, but they're definitely a goal since they're needed e.g. for better async support.


Rust has a flavor of inheritance through Traits and #derive.


No, traits are not inheritance, nor is derive that is default implementation of some traits.

Traits are like interfaces in OOP languages (simplified), the equivalent of a class would be a struct + all the impls of that struct. And that thing cannot be extends, a struct is fixed when defined and nobody can extend it, you can use composition that is the correct to me way to go.


That's why I said "flavor". It's not exactly inheritance obviously, but provides some of the same functionality. There are many ways to skin an OO cat.

Traits describe the methods implemented, and you can override with specific implementations or inherit the default, from a level up.


Traits implement interface inheritance, yes. #derive has nothing to do with derived classes in OOP, it's an annotation-like facility that adds auto-generated code to any object.


Isn't #derive a syntactical sugar for a bare impl?


> prefer composition over inheritance

Right... There is an animal in a dog.


Last time I checked, C++ used function pointers behind the scenes to achieve virtual method dispatch.


Typically the object contains a single pointer to the vtable which is then the list of function pointers.

You can do this structure in C, but more common is the struct of function pointers which avoids a level of indirection at the cost of object size.


But how do you tackle creation of objects, call malloc everytime?

And how do you for example, create an array of objects, like one can easily do in c++?


Or you could implement COLA?

https://en.wikipedia.org/wiki/COLA_(software_architecture)

Huh, that page was deleted in Dec 22. "concern was: Old research project. Unable to locate any details. VPRI institute is dead. Been on the cat:nn list since March 2009. No new updates."

Goddamned deletionist activist WP editors tearing down the human knowledge base.

(If you don't know VPRI is (was?) Alan Kay's research org. I think it's a bit notable and important.)

ANYWAY

From the search result (in DDG) snippet I can get the first two sentences of the deleted page:

> "COLA" stands for "Combined Object Lambda Architecture". [1] A COLA is a self-describing language in two parts, an object system which is implemented in terms of objects, and a functional language to describe the computation to perform. [2]

It's a very simple system that gives you the basis for both OOP and Lisp-like semantics. It's fun!

You can see it here: https://piumarta.com/software/cola/

Or check out the VPRI reports, etc.:

https://web.archive.org/web/20220819075633/https://www.vpri....

Ironically their website appears to be down at the moment.


While I commend this effort I should say that seriously, just write C++ if possible. No matter what you do memory management is going to be your biggest enemy (I’m ignoring the aesthetic aspects like macros, function pointers, type safety, etc). Without RAII it’s just not worth it. Just choose the simpler portions of C++ and keep safe.


I never have trouble with memory management in C by sticking to "grouped element architectures" with explicit lifetimes such as arenas, scratch allocators, dynamic pools, etc. RAII is mostly for "single element architectures" which IMO are not a good way to organize code because they tend to amount to thousands of unnecessary memory operations.


You can do RAII in C. You still have to define a dtor function.

https://en.wikipedia.org/wiki/Resource_acquisition_is_initia...


I am aware of this "non-standard" extension. My comment was w.r.t sticking to the standard.


Is "dtor" shorthand for destructor?


Yes, just like ctor is constructor.


Yes! You got it.


Yeah, I feel like “C in a C++ Compiler” is a respectable decision for reasons similar to what you describe.


There is a lightweight object oriented extension to C called Objective-C [1] that unfortunately never gained much traction outside the NeXT/Apple ecosystem. There is also Cello [2].

[1] https://en.wikipedia.org/wiki/Objective-C

[2] https://github.com/orangeduck/Cello


I was surprised no one here mentioned cfront. My first job out of college I would look at the generated C code from my C++ source to see how it would convert some of the the C++ things into C, since I couldn't figure out how to do it myself.


The famous htop actually employs OOP in C. https://github.com/htop-dev/htop/blob/650cf0f13bf667270d0a6a...


So does Glibc in its stdio implementation (bog-standard vtables except the vtable pointer can occasionally change at runtime), Linux in a number of places (I don’t really know much there), the FreeBSD kernel in its driver ABI (more like ObjC than C++) and many others.

(GObject is of course also object-oriented, but IMO hardly counts as C programming in how it works, it’s more of a separate OO language hand-translated into C—Vala is basically that language, finally implemented years later.)


I was doing OOP in C, back in the early ‘90s. Lotta work, but turned out great. Some of the software that I wrote back then, was still in use, 25 years later (a camera software SDK).

It had to be done a certain way, because stack conventions were all over the place, back then.


In early 90s we’ve got a CD-ROM with all known genomic data from NCBI. And on the same CD there was a portable (Win, Macintosh, X Win) graphics system written in C in OOP style, named Vibrant. Portable graphics long before Java! And using that funny - and very usable - OOP style too. So, instead of studying genomes I was looking at C code, and soon was using it in a telecom project that I was doing on the side (90s were pretty hard in Russia).

https://www.ncbi.nlm.nih.gov/IEB/ToolBox/SDKDOCS/VIBRANT.HTM...

The code is pretty well written and probably can be used in a teaching environment, like the original article describes. But I am very bad in teaching…


I recently needed to write C code to interface with a COM library on Windows. While Microsoft fully supports it, the result is very foreign compared with typical C code.

Surely in C one does use tables of function pointers to implement some aspects of OOP. However, a good library limits it and rather exposes some structs instead of requiring multiple virtual methods calls to archive a similar effect.


It depends a lot on the wrapped APIs, for instance D3D11 is a struct-heavy API exposed via COM and totally fine being used from C code (instead of C++), even works nicely with C99 designated init for initializing the input structs.


My experience was with MFC, that uses methods even for basic getters.


I think arrays of 64 byte structures is the most optimal way to architect your multicore "on the same memory" code.

Preferably using int/float which should be atomic on X86 and hopefully on modern ARM too, don't know about RISC-V yet.


Strongswan, the open source IPSec VPN software (https://www.strongswan.org/) is written in object oriented C. Ref: https://docs.strongswan.org/docs/5.9/devs/objectOrientedC.ht...

I have not looked through the source myself but thought this might be an interesting reference.


I can confirm that indeed Strongswan is written in OOP C.

This is relatively common in software with a modular architecture that is intended to run on embedded systems and whose design dates back from a time when C++ standard library was not really well supported on such systems. This is also useful when performance is highly important and you can't afford the C++ vtable overhead.


I've seen OOP inheritance in C before. It involves a table of pointers to functions, and a derived "class" would just point to a different table.

It works, but all the machinery has to be handled manually. It's very, very easy to make a mistake. The question is, why do this? Just use C++ as "C with classes" and you'll be much better off.


The goal of this is to understand how OOP works under the hood, so you can adapt it to your own use case. You can do more strict OOP than C++ if you wanted to. However in most cases, all you need is basic OOP, namely writing your code with clean separation of functionality in different files with `static` keyword being similar to what `private` means in C++, and then using structs with typedef to define data. Function pointers and additional macros just add syntactic sugar, and are not necessary.

The main advantage of C++ isn't really OOP, its all the standard library features and smart pointers, but those are not always applicable (like when you want to manage memory as efficient as possible). So using C is often preferred.


the implementation in the article is basically what Golang is.

and the question: "so much manual/repetitive work, why do this"... is still valid.


Yup. Once you find yourself doing OOP in C, or doing metaprogramming with the C preprocessor, it's time to move to a more powerful language. You've outgrown C.


I see so many uses of the term ADT that I can't really give anyone a confident definition


Using ADT to mean “abstract data type” does not mean anything more than what most people mean when they say “type”. Outside of distinguishing from other meanings of the word “type” there is no practical reason to ever say it. It doesn’t sound fancy, it just sounds like getting high on your own supply of acronyms. It is not an important term to learn as a beginner in the first chapters of a programming book. We should stop using it in resources like this.

Otherwise, ADT can also refer to “algebraic data type” which means the ability to compose types together thereby adding or multiplying the different values the resulting type can take. Product types (aka structs) multiply over their fields; two int fields when taken together can take on (# different values of int) * (# different values of int) different values. Sum types add over their variants: a Rust enum “enum OptionalInt { Some(i32), Maybe(i32), None }” can take (# different values of i32) + (# different values of i32) + 1 different values.

Most languages have structs and that’s the “product type” sorted, so ADT generally refers to a language having tagged enums. C doesn’t have that but you can emulate it (quite badly) with unions and enums. Good examples of languages with ADTs are OCaml, Haskell, Rust.


> Using ADT to mean “abstract data type” does not mean anything more than what most people mean when they say “type”.

Abstract data type is a type where you don't get direct access to the information contained in it. The encapsulation is what makes it "abstract".


We should call it an opaque data type then.


That thing was named much before the other ADT became popular. The name is spread over a lot of past texts, and not in wide use anymore.

There's no point in changing anything.


Is a Java ArrayList considered an ADT like stack, queue, set, etc?


In the context of C programming, ADT usually means an opaque struct with some related functions.


An abstract data type (ADT) is a structured type for which a client cannot access the data components for variables of this type. The client understands the type in terms of its operations, provided by a set of functions which operates on variables of this type. The set of functions is called the interface of the ADT. Note that this definition requires information hiding but not inheritance or polymorphism.


How would you distinguish that definition from one for "type"?


> How would you distinguish that definition from one for "type"?

The crucial difference between an abstract data type and a concrete data type is that the content is hidden in the former case.


all that memory map stuff is completely out of whack, along with much else


Beside the topic or the content, I love the typography choices of this.



TLDR: Put data members in a struct. Write a function per "method" and pass in the `this` pointer. Simple and effective.

BUT: inheritance. C++ has subclasses (`class Manager : Employee`) and virtuals / vtable lookup on pointer access (`employee->name()` which calls `Manager::name` or `Employee::name` based on type).

What is the typical equivalent in C? Hand-rolled vtables? Or is there a general paradigm that helps to avoid the need for it in C?

I ask as a seasoned C++ and Java developer looking to improve some C code which uses this pattern in the extreme and needs some refactoring. My intuition is composition rather than inheritance, although isA is more intuitive than hasA for this code-base.


Hand rolled vtables in a struct that has metadata for the class. SQLite has a nice system where the class definition struct carries the size of the base class and any additional members so that objects can be allocated from common code and passed to their constructor method.


You're going to think this is a joke but I'm mostly sincere The right refactor is to untangle the data structures, write a lua interface to them, then port the application logic to lua. Since (one of the) lua is written in C your codebase is still all C.


ADTs was a major part of the C course I took in 1998


so-called abstract data types have nothing really to do with OO programming


I was thinking the same thing.


Unrelated to the content, but this is basically a perfect webpage.


Ok, but this is how pretty much all large programs were designed in assembler. C has the data structures commonly used in assembler. C++ (originally) removes some boilerplate and adds some comple-time checking.


[flagged]


Hi. For background, I have been writing professional OOP code in ANSI C.

Yes, Structures in C can be used as classes. And yes, this includes polymorphism.

My solution was to put a callback in the structure that would point to the implementation that was polymorphic. This allowed me to pass the struct around and then call the specific implementation at the right time.

When you write OOP in C you do not need to make it look like OOP language. You just need to understand what OOP is about and use the existing language to do what you need it to do as efficiently as possible.

As to spending time on writing functionality that is readily available in other languages... sometimes you just don't have a choice. There are programming environments where ANSI C is the only language available as was in my case.

Please, try not to be too dismissive to the things you do not understand.


>My solution was to put a callback in the structure

I think you mean function pointer. But yeah, that's the usual way to do it, although a bit wasteful in terms of memory compared to a vtable (but needs one less indirection to jump to the method).


A function pointer is the mechanism by which you realise a callback.

For me, a callback, is when you have two modules and the function pointer is used to call back the functionality from the calling module in an environment and at a time determined by the called module.

A polymorphic method is a callback used in a certain way in context of OOP. You have a callback from one module (the implementation) passed to another module that can call potentially many different implementations through the same interface (which makes it polymorphic). This callback is tied to the structure by the calling module, which makes it a method. You may have language support and have entire mechanism hidden from you, but this is essentially what is happening. A pointer to function that is passed to another module for execution.

Now, when you do "manual" OOP in C without building your own vtable macros and such, I think "polymorphic method" is a bit too much and "callback" is more fitting.


> Please, try not to be too dismissive to the things you do not understand.

I never said it's impossible to do method polymorphism in C, I said nowhere in that article method polymorphism is implemented.

You obviously did not read what I wrote at first place, only saw what you wanted to see to make some kind of (bad) rebuttal without a single concrete implementation as an example. Of course it's possible to implement method polymorphism in C, which is what I said, the article linked isn't doing that anywhere, quite the opposite.

So no, C structs are not equivalent to Java classes, at all.


I said nowhere in that article method polymorphism is implemented.

Well, not quite. You said that it isn't OOP without method polymorphism. So no need to fly off the handle and accuse others of misreading something you didn't write.


> Well, not quite. You said that it isn't OOP without method polymorphism. So no need to fly off the handle and accuse others of misreading something you didn't write.

I said

> This isn't OO until you implement some form of method polymorphism.

which means the article didn't implement method polymorphism.

> So no need to fly off the handle and accuse others of misreading something you didn't write.

So you need to pay attention at first place instead of "flying off the handle" yourself and then accusing others to do so in a useless comment.


Yes, the article is terrible as it does not show any polymorphism. Also, yes, doing genuine polymorphic OO in C ends up horrible because you invent a new language. (Source: many years working on a truely evil code-base of OO C. Originally the work of one twisted genius. Brilliant, but evil).

OO was just a failed paradigm anyway, I’m amazed anyone would try and shoe-horn it into C in the modern day.


I don't think OOP has failed at all. It has become the foundation of all reusable code in every language. What failed was the idea that internal state can be managed and shared with derived classes.

The key to OOP is extracting the idea of types/concepts into interfaces that may have different implementations.

You don't even need a language that supports polymorphism as a first-class concept in order to do this, though it is certainly much safer. Python chooses duck typing, modern C++ uses "concepts," etc. You can do SOLID under any of this. Some languages or frameworks take it too far and think of everything as an instance of a base object, for no great reason.

I guess a summary of my thesis is that the "L" in SOLID is what makes an architecture OOP.


Eh, this is a very separate debate. You don’t need OO for modularity. This should be obvious. You need an API/Contract and for re-use you need a “module” (such as a dll or a web interface). For example something like:

int CreateTheStatefulThing(blah* thing); DoStuffWithThing(blah* thing); void CleanUpThing(blah* thing);

To suggest OOP is the foundation of reusable code is simply wrong.


You are reading in to what I wrote something I did not say.

I said that "OOP has become the foundation," not "OOP is the foundation." There is a very big, historical, fact-based distinction between those two statements.

Modularity and reuse are completely different things. A highly modular codebase is more amenable to write reusable code, but many modular systems have been written with very little concept and code reuse.

Of course there was a measure of reusable code before OOP was a more formalized language for discussing it. But to say "you can do these things without OOP" is like saying you don't need structured programming to write maintainable systems. And it's like "ok, but you're still using "if," possibly "return," and you know... functions." Essentially the exact same ideas that the paradigm gave formal language to.

I think people who say that OOP wasn't responsible for the understanding of modern paradigms haven't read much of the theory behind it and have hyper focused on the poor series of books and articles that came out from the 1980s through about 2010 that encouraged hidden state ownership and inheritance of state.


Anyone who disagrees with you is simply poorly read and uninformed?

You cannot separate theory and practice in the way you are attempting to. It’s not just “bad books” that lead to the abuse of inheritance that is so familiar, there were (and are) broken concepts that only became visible with time.

I highlighted “module” for a reason, because one of the key mistakes of practitioners was to view the element of re-use as the “class”. In reality if we want to create a reusable component the boundary is much higher. We create an api and a library or some other component that can be distributed.

Building a reusable component is much more work, and recognising when that is necessary and designing accordingly is an important engineering discipline.

To finish with a snipe of my own; I think people who take an OOP is primary view of the world need to do a little more programming in lisp to properly understand the fundamentals.


Your straw men fall easily. But your "snipe" doesn't even apply. I spent the two years of my masters program editing a NASA-owned LISP codebase. it wasn't huge. Maybe 100kloc. But I'm definitely not ignorant or LISP.

Look, you don't like OOP, who cares? I certainly don't. But that's different than trying to explain that you have a fundamental misunderstanding.

Whether you accept it or not, it's in your head now ;)


I’m too old and tired for the insult slinging anyway.

What’s my fundamental misunderstanding? What’s the straw man? Class == re-use?

I don’t hate OO by the way, it’s just not the first, only, or in my opinion best way to think about or structure code. I do think the expectations that OO would bring increased productivity, code-reuse, etc were not realised and hence why I referred to it as a “failed paradigm”. Not because it is bad from a theory perspective, but because in practice none/few of the things it was expected to deliver actually materialised. And also that the real challenges with “code reuse” are different. I have read the bad books, but also the good books.

I’ve done more OOP programming than anything else over my career, and it can be done well. I’ve also worked with and coded in extremely well written C code bases. I just can’t see the value that OOP provided over modular C. Perhaps you can enlighten me?


I can agree with a lot of what you wrote, and especially outcome vs. the original OOP promises. They meant one thing, and it turned out that thing is actually an anti pattern (inheriting implementations and state, not merely inheriting interfaces), but then they also delivered on what they said, just in a different way. Example: "we will have reusable code!" What they meant was deep inheritance hierarchies where children just inherit their parent code and selectively override parts of it. What was delivered instead was a set of principles using the vocabulary of OOP to guide developers into designing interfaces that are highly reusable (SOLID).

In terms of what this adds to C, I can only suggest that you study a large C project. I'd recommend the Linux kernel. You won't find first-class support for inheritance and polymorphism, but it's there. It is amazing how modifiable and extensible entire subsystems are, and when you examine it, you find the SOLID principles at work, which came from OOP study and practice.

I don't really understand what you mean by the way OOP is "structured." If you're referring to deep object hierarchies like Java or Qt, I don't really disagree. They attempt to force an architectural unity through object hierarchies. But then on the other hand, if you're saying that it isn't helpful to contemplate an architecture through the lense of types, the types of transformations and operations that can be performed on them, and packaging these operators and transformers into a group that can be passed to code that calls them abstractly, then I don't agree.

Amusingly, I don't think that second thing is what you're saying. But with your arguing against OOP, I don't really know how to interpret this. This is what OOP gave us language to describe. Of course some of these techniques existed before in LISP or C, but code bases of the day were largely NOT leveraging them in any large scale way. OOP gave us vocabulary for these concepts, as well as mainstreamed their use, similar to how functional programming mainstreamed the idea that a nonlinear or cyclic graph of executable functions must not allow shared state, even if hidden within objects, if you want to be able to maintain that code long term, and extend it. So it seems like you're arguing that OOP is bad, while neglecting the history that OOP brought us the common understanding of how modules should interact with each other.


I think we have been talking past each other to some extent. What you are defining as OOP is not what I think of as OOP. The concepts that you are referring to as valuable I agree with, I just think they are independent of OOP.

I have previously worked on very large C code bases (10m+ loc) that were well written. I haven’t worked with the Linux kernel, but the “SOLID Principles” and “modifiable and extensible subsystems” certainly were evident in these code bases. You claim that this form of programming came as a result of the study and practice of OOP, but I think this cannot be right. The ideas pre-date this. These code bases, pre-date this. Much of the literature from the 70s covered these topics.

I think what happened through the later 90s/early 2000s was that OOP was universally embraced, but without theoretical backing. C programmers picked up C++ (I was one), without really understanding, and the result was a mess. From there I think good practices were rediscovered, not invented. I don’t credit OOP for the good practices, although I don’t disagree that some truly new things have been learned.

However, I still have to deal with the poor practices within OOP. Developers inherit all the time for convenience. Rarely do they think in terms of interfaces/contracts at the modular level (why is this class public? *shrug* why isn’t this class sealed? *shrug*) It’s this sort of thing that makes me annoyed at OOP. To this day, developers still sometimes think of every class they write as being a “reusable” component, and this I lay at the feet of OOP.

Classes and Objects are sometimes useful. That’s what I think of as OOP, and that’s the bit that I don’t see as very valuable.


Guys, chill out.

"OOP has failed" should be read in context of the promise that everything can be effectively implemented with OOP, that you can make objects for everything and your entire program built out of it. The masses adopted it with pretty poor results -- and the poor results prompted people to say that "it has failed".

It just means that it has failed to live up to its promise. It does not mean OOP is not useful. Quite opposite, we have learned a lot about when it is very useful to use OOP. And also when it probably is a poor idea.

I personally mix and match OOP and functional styles. I will typically create objects to describe domain model because it is super useful to talk about operations on entities. But I will call these objects from functional context where it makes much more sense to create infrastructure from functions that can easily deal with different types of data.

No programming paradigm is clearly better than others and no programming paradigm is without flaws.

I would strongly suggest treating every person that says otherwise with extreme caution -- people tend to say these things at certain stages of their evolution as programmers until they have enough time to learn ins and outs of all things.



This isn’t a religion. I really don’t see what is so controversial about suggesting OOP has clearly failed on the majority of its original stated goals.

That’s not to say that nothing has been salvaged from the wreckage; languages like Rust are a great example of retaining what actually was useful.


>I really don’t see what is so controversial about suggesting OOP has clearly failed on the majority of its original stated goals.

Because it is not borne out by facts. The explosion of software since the 80's(?) was a direct result of OOD/OOP becoming the dominant paradigm.

Your posts remind me of this: https://twitter.com/awbjs/status/625082965257654272


So my views are hopelessly uninformed opinions, while yours are undeniable facts?

Please do go on and prove how the explosion of software since the 80s was the direct result of OOD/OOP. Please be sure to reference Linus, Joe Armstrong, Alexander Stepanov. Surely such undeniable facts will be supported by relevant leaders in the field?

This quote from Eric Lippert clearly sums your position well “ “It may be that this success is a consequence of a massive industry that supports and is supported by OOP.” which is true by virtue of it being tautological.

We can do better than a tautology to answer the question posed in the title of the piece by observing that OOP style directly and deliberately maps well to the typical hierarchical organization of large corporations. Why do we hate working in OOP? For the same reasons we hate working for inflexible, siloed, process-heavy, policy-heavy, bureaucratic, mutually-distrusting teams in corporations. Why is it a success? For the same reasons that those corporations are successful: organizations with those characteristics have historically been strongly correlated with creation of value that increases the wealth of the billionaire class, and therefore billionaires pay for the creation of those systems.

It should be no surprise that OOP style is popular in corporations; OOP is the reification in code of corporate thought patterns.”


>So my views are hopelessly uninformed opinions

In a sense, Yes; because you are the one who made blanket statements like "OO was just a failed paradigm anyway," which is objectively wrong.

To elaborate on my comments, almost every language since the 80's has had very good support for OOP. Almost all reusable libraries/frameworks (especially GUIs) also are implemented using the OOP paradigm so much so that nowadays the kids don't even know of a world before "Objects". The success of OOD/OOP in the industry is self-evident.

If certain aspects of OOD/OOP have to be used carefully (eg. Implementation Inheritance) and how abuse of OOP (eg. everything is an Object) can make a mess of things, then that can be debated, but to dismiss the whole of OOD/OOP is just plain silly.

Coming to your "appeal to authority", let me clarify the nuances in their writings/quotes.

Linus Torvalds: His objection is not to OOP per-se (indeed the Linux Kernel uses a lot of OO patterns itself; see the articles by Neil Brown on lwn.net; links in my past comments) but certain aspects of it and in particular; to the C++ implementation of those. These are valid for his Kernel but not for others.

Joe Armstrong: His focus was on "Concurrency Oriented Programming with no Shared State". From that pov his design of Erlang "Processes" is exactly as Alan Kay envisioned OOP. Here is Armstrong himself: https://elixirforum.com/t/the-oop-concept-according-to-erlan... More here: https://stackoverflow.com/questions/3431509/is-erlang-object...

Alexander Stepanov: His background is in Mathematics and hence he looks at Programming from the pov of Abstract Algebra. His issue is with straitjacketing "Interface Inheritance" within a single type hierarchy whereas logically a subtype can span multiple hierarchies. This is also the reason Bertrand Meyer called "Multiple Inheritance" indispensable.

Eric Lippert: Your listed quote doesn't make sense, i am almost sure he was saying it "tongue-in-cheek" to draw attention to the fact that dysfunctional OO codebases are similar to dysfunctional organizations. It is not a value judgement on OOD/OOP.

In summary; OOD/OOP is a way of structuring Procedural/Imperative programming where global state is partitioned into smaller pieces along with their transformers which helps you manage Modularization, Types, Type hierarchies, Modeling real world "Objects", Reusability, Extensibility and Maintenance.


Well, maybe. But that is a different debate. I do not see where this topic was raised, except by you right here.


please post an example of what such a callback would look like


Linux kernel code makes heavy use of object-oriented design patterns [0]. It's not the best of its kind, but still provides a reasonable example of how OOP can be achieved in C: https://gist.github.com/cakturk/cd75d0ca588151c86d641cb6d5a1...

[0] https://lwn.net/Articles/444910/


Part 2 to the above lwn article: https://lwn.net/Articles/446317/



Polymorphism in C is trivial: You use a vtable - a struct of method pointers. Same approach most languages with "simple" method resolution mechanisms, like e.g C++, uses under the hood.

You don't need macros. You either call via the vtable, or you write a wrapper per interface per message.

E.g.

  struct class {
    void (*vtable[NUM_SLOTS]);
  };

  struct object {
    struct class * class;
  };

  typedef struct object object;

  const int STRING_LENGTH_SLOT = 0;

  int string_length(void * self) {
    return ((int (*)(void *))((object *)self)->class->vtable[STRING_LENGTH_SLOT])(self);
  }
(EDIT: I forgot just how much gross casting is needed, and added examples of class and object)

Such wrappers are fairly trivial to generate if you don't want to handwrite them.

If you want complex inheritance patterns, you might benefit from one more level of indirection, see e.g. Protocol Extension: A Technique for Structuring Large Extensible Software-Systems (M. Franz, 1995). That paper is for Oberon, but it's easily translatable to C - did that way back.

Upside is it let's you group functionality in interfaces with less of an explosion in vtable size, downside is the cost of one extra pointer indirection.


> You don't need macros. You either call via the vtable, or you write a wrapper per interface per message.

I know, thus the /s for sarcasms.

> Such wrappers are fairly trivial to generate if you don't want to handwrite them.

It's fairly irrelevant to my point, which is that C structs aren't equivalent to Java classes in anyway.

Of course you can implement OO is C, I've done enough GObject in my life to know how horrible it is to use in practice.


They only way they aren't is that the class pointer is hidden from the user.


Nothing in your example looks or reads as trivial. That is a gross perversion of a simple procedural language.


Trivial to a C developer then. This has been typical of C code for literally decades. The only thing that sucks there is writing wrappers, but it's easy to generate them.

From a user point of view you end up doing things like:

  string * str = string_new();
  int len = string_length(str);
So it looks just like most other typical C code using prefixes per interface and passing pointers to structs around.

In practice, like in C++, you'd tend to only actually use the vtable approach for those things which you actually need to be able to override based on type, which tends to be rare.

Another approach is to use messages instead, and a call a generic message handler. E.g. "ob->call(MSG_TYPE, ... arguments)" and just have a pointer to a message handler function as the first pointer in your objects. It's more flexible, but slower.


"int string_length(void * self) { return ((int ()(void ))((object *)self)->class->vtable[STRING_LENGTH_SLOT])(self);"

Oh.My.God.

At this point, you might as well be using Java.


If you're writing C, you'll be writing much worse than that on a regular basis. There's a reason I rarely use C any more.

But of course if you're going to reimplement OO from scratch in C you will see the entire machinery laid bare. You're only doing it in the first place either because you're committed to using C for whatever reason, or to understand OO implementation strategies.


Replying to myself to leave this here, since it's vaguely related. If anyone really want to have nightmares about OO'ish concepts in C, here is a few different ways to implement closures in C, going from relatively straightforward, to (unportable) runtime generation of assembler thunks:

https://hokstad.com/how-to-implement-closures


You are just being glib here.

If you want simpler models see my other comments in this thread.


Or C++

/gasp


what does an "object" look like? how do i derive from it?


Easy; a subclass struct embeds the superclass struct as its first member.

Simple example here: https://embeddedgurus.com/state-space/2008/01/object-based-p...

Lots more here: https://stackoverflow.com/questions/351733/how-would-one-wri...


Having spent quite some time down that rabbit hole back in the days, I'd definitely recommend against pushing C in that direction. Few people love GObject, for good reasons.

Embedding combined with the container_of-macro [0] is a much more constructive approach from my experience.

[0] https://stackoverflow.com/questions/15832301/understanding-c...


Any struct. Only requirement would be that the first element can be casted to a point to a class object containing the vtable. I'ved edited to fix the casting so it actually compiles, and to use a void * in the argument list, as is what you'd actually usually do.


generic base type struct


All you need is a field in the struct to point to your vtable. Please try to think more charitably and not be so aggressive. Even if the author was wrong, which they aren't, doesn't mean they don't have the kernel of a good idea.

Charitability is not a tool merely for being nice to others, but being nice to yourself. You learn more this way.


[flagged]


"plagiarized argument"

I'm just curious what I plagiarized.


> "plagiarized argument" > I'm just curious what I plagiarized.

You perfectly understand what "passive-aggressive" means. Just don't go around patronizing people like that, nobody died and made you god.


You won't believe this, but I was neither attacking you first time, nor "passive-aggressive" the second time. I asked a question I didn't know the answer of, and I wanted to read what you think I copied. I'll live without the answer.


Agree. What the author did here is just plain data types, not even abstract data types. Another big mistake in the article is

>Classes in an object oriented design are fundamentally ADTs

It isn't. I would suggest the paper Object-Oriented Programming Versus Abstract Data Types by William R. Cook which explains the fundamental differences between the two data abstraction techniques.

edit:

https://www.cs.utexas.edu/users/wcook/papers/OOPvsADT/CookOO...


I believe the author was trying to drive the point that structs in C are the most similar language feature to classes in Java rather than implying the two are identical. Perhaps the title could’ve have been written less absolutely, but declaring the entire article as “terrible” simply because a single title was unclear is simply an exaggerated criticism.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: