Hacker News new | past | comments | ask | show | jobs | submit login
Object-Oriented Programming in C (2019) [pdf] (state-machine.com)
87 points by danielozcpp 13 days ago | hide | past | favorite | 92 comments





Back in the '90s, "Object-Oriented" got redefined in popular imagination to "good". If your language or system was good, it was then by definition object-oriented. Anything good had therefore to be called OO. Saying something was not actually OO (e.g. this) was taken to mean it was not good, generating spurious conflict.

The fiction has largely dissipated, except among pundits and the aggressively ignorant. Meanwhile, a backlash insisting OO is a fundamentally mistaken design element arose among some loud users of not-OO (not to say not-good!) languages. Even among users of what were called OO languages, C++ particularly, the relative importance of OO techniques has fallen off as other language facilities and conventions surfaced.

C is just not adequately equipped for what is formally defined as OO. You can cobble up a dodgy simulacrum of it, with enough effort, but what is the point? Other languages make it easy, and can invariably be used in place of C if you really feel like you want OO.

If you are coding C++, you probably long ago graduated from the notion that "OO" has any particular merit as an organizing principle, and simply use language features to achieve your goals. Some part of any big-enough system will look OO, more or less, if you care. Few working C++ programmers do.

Some languages, Java particularly, aggressively try to horn everything into the OO shoe. The result is that if what you want to do doesn't fit, you must abuse what features the language offers to achieve what you mean to do, typically with some violence to OO norms. There's nothing wrong with that. It was, rather, wrong for the language to provide you with only OO facilities to the exclusion of all else.


> Back in the '90s, "Object-Oriented" got redefined in popular imagination to "good". If your language or system was good, it was then by definition object-oriented, and anything good had therefore to be called OO. Saying something was not actually OO (as we find here) was taken to mean it was not good, generating spurious conflict.

A similar thing happened with functional programming.


thank you, it doesnt matter what brand of hammer you use aslobg as the quality is sufficient.

https://microsoft.github.io/microsoft-ui-xaml

> WinUI is powered by a highly optimized C++ core that delivers blistering performance, long battery life, and responsive interactivity that professional developers demand. Its lower system utilization allows it to run on a wider range of hardware, ensuring your sophisticated workloads run with ease.

Apparently Microsoft does care about OOP in C++.

As does Apple,

https://developer.apple.com/metal/

https://developer.apple.com/documentation/driverkit

And Google,

https://github.com/google/oboe

https://www.tensorflow.org/api_docs

Maybe modern C++ is safe from OOP, so lets look into ranges,

https://en.cppreference.com/w/cpp/ranges

So we have factories, adaptors, concepts (aka interfaces/traits in other languages), std::ranges::view_interface as base class mixin, all stuff I can find on the Gang of Four book.


I wasted time looking in those links. The WinUI page does not mention base classes (or prototypes), inheritance, or any sort of runtime binding. A "highly-optimized C++ core", in particular, does not imply OO. The std::ranges view_interface template is not OO; it uses concrete inheritance purely as a notational convenience.

Likewise the others, with one exception. The Apple driverkit page does mention base classes.

Factories, adaptors, concepts/traits/interfaces have nothing to do with OO, beyond that OO designs often also use them. OO designs define functions, too, but functions do not imply OO.

It is, in any case, meaningless to trot out apparatus system-vendors oblige programmers to use to access proprietary facilities, and equate that to programmers' interest in whatever tech is used for them. Programmers are interested in using the facilities, and are glad when whatever they are obliged to use works at all; too often it doesn't.


WinUI uses COM/WinRT, COM is an OOP ABI, and WinRT is basically COM v2, with TLB libraries replaced with .NET type system and types must support IInspectable in addition to IUnknown. Are you going to argue .NET isn't OOP?

As for the rest, OOP doesn't imply inheritance, BETA and SELF are good examples of OOP without inheritance.


Nobody uses Beta or Self. And, no matter how much alphabet soup MS throws at its long, rolling UI debacle, it demonstrates nothing about general programmers' current level of interest in OO.

Arguing against industry usage of OOP, including C++20 own standard library, trying to fit one's little window of the world, also doesn't help.

OOP is great when you write APIs/libraries (your examples), but is useless or even counter productive when writing applications, where encapsulation or inheritance just obfuscate everything.

My main grip with Java is that everything is by default a library.

But there is hope, recent changes in Java, records and pattern matching move away from that idea.


saying OOP is only useless and counter productive is as short sighted as saying it is the golden hammer for everything.

PS: WinUI apps use plenty OOP


It's not what i've said, OOP is great for creating APIs like WinUI, not great for writing applications

In which universe an application which uses mainly OOP APIs isn't itself OO.

What OOP is good at is maintenance over the time by making clear what is API and what is implementation.

My point is that when you develop an application, you only need strawman OOP i.e. calling methods.

You don't need class inheritance if you can pass functions as parameters, you don't need encapsulation given all your pieces co-evolve at the same time (if it's not split than part into a library).


and WinUI apps are applications, and - as said - OOP is used by them, see e.g. the reference architecture guide

> Back in the '90s, "Object-Oriented" got redefined in popular imagination to "good". If your language or system was good, it was then by definition object-oriented. Anything good had therefore to be called OO. Saying something was not actually OO (e.g. this) was taken to mean it was not good, generating spurious conflict.

Excellent point, and I think in our contemporary age something similar is happening with "Agile". It has ceased to become a project management technique useful for a certain class of products. It has instead become redefined to mean "good" - for example, in popular imagination, if a team is not "Agile" then they are somehow backwards or out-of-the-loop. Now teams, products, and situations where Agile is not appropriate are finding themselves having it forced upon them, or spinning what they do as agile.


Agile has meant 'bad' for years now ever since Agile processes were formalized and adopted by big orgs. At least that's how I've thought of it: 'Agile' means the worst of all worlds. Run if you can.

Well it's become the exact corporate management hell that the original manifesto authors sought to fight back against. I personally avoid joining teams or companies that drank the kool-aid, and additionally find agile to be a particularly counter-productive way to deliver value to customers quickly. About 7 years or so ago I had the first inkling things were wrong when "Agile Coaches" appeared who were not developers.

It does, at least, facilitate micro management and burnout.


The rule of thumb I've noticed.

If they use "agile" as a noun, they generally have no idea what they're talking about and you should be careful.


I think, everybody forgets one thing with C OO systems - no matter how primitive or advanced they are, they integrate much easily with languages that are sharing C libraries, than e.g. C++. Take, for example, GObject [1] from glib.

C++ had, for a long time, problem mapping classes and objects in python or ruby, and usually, that was done via C wrappers and hacks. In recent years, things are a little better thanks to clang, but there are still tons of template hacks and C workarounds if you want portable code between compilers. Even then, you often rely on compiler specific stuff.

Other languages are much worse, so the only solution, frequently, is to use a virtual machine, like JVM or .NET.

[1] https://en.wikipedia.org/wiki/GObject


The paper does not specify message passing or reflection, both of which are essential to OOP. I think OOP lost its way as soon as C++ reinterpreted it in terms of only encapsulation, inheritance, and polymorphism.

According to Alan Kay (one of the creators of Smalltalk, the first OOP), message passing is more important to OOP than inheritance. If you listen to his old speeches, what he is describing sounds a lot more like microservices and VMs/containers than what most of the later languages turned it into. (He describes objects as "mini computers" that interact with each other using only public interfaces)


The first OOP language was Simula. Smalltalk and C++ both adopted ideas from Simula, independently. Bjarne Stroustrup was a student in Nygaard's lab, so it is absurd to suggest that C++ "reinterpreted" something. If anything, C++'s is the more pure expression of the original idea.

Alan Kay gets credit for the name "object-oriented", not the concept. His own definition has varied radically over the years, insisting only lately on any importance of message passing, as such. Smalltalk-72 was not OO. Smalltalk gained OO features over the time from 1972 to 1980. (Message passing has anyway always been isomorphic to function calls.)

Reflection was never described as essential to OOP. It is a feature of many languages, equally useful in all.


If you need to do OOP in C, it's time to move to a more powerful language.

There is one advantage to doing OOP in C. Once you figure it out, the mystery of how OOP works all falls away. Frankly, I didn't understand how OOP worked before doing that. None of the tutorials explained what was going on under the hood.

As an engineer, I'm never comfortable using something when I don't know how it works.


While it's hardly a complete object system, perl's bless() operator associating a package name with a data structure, where the package is (for bless()'s purposes at least) basically just a global hash mapping method names to subroutines, was pretty much how I first got my brain around the concept.

I wish more "introduction to OO" things would start by demonstrating a dispatch table and then showing how the vtable concept maps onto that, I suspect it would make things significantly clearer to a bunch of people as they learn.


> it's hardly a complete object system

While other programming languages became mired in OOP dispute, opinion-battles, and all the ensuing problems[1], Perl 5's built-in OO is easy to learn and understand and makes the difficult things simple. It has been stable and effective for decades.

OO is just a pattern; it can be useful or misused, some aspects are more useful than others. But the defining goalposts are moved all the time. They tend to follow the hype machine.

[1] Members of the Perl community know that OO discussions have become harsh there too.


Yeah, I might know a little bit about that, but I was focusing on how bless() helped me initially learn how OO worked at all. (I was after all one of the first power users of Moose, am the original author of Moo and Role::Tiny, and (re)wrote several chunks of the Mojo::Base OO system at various points as well ;)

I'm pretty glad we've mostly standardised on the M* style these days, and Object::Pad (the cpan module that will likely eventually become the template for core OO) is still pretty familiar to users of that as well.

(the days before M* style became the default were ... "fun" ... though at least it meant we got native C3 MRO support added to the perl5 VM to support my decision to use a C3 based component model when I wrote DBIx::Class)


Hmm, let me guess, it begins with a D?

Dart? /s

Thats's silly. OOP is a software engineering paradigm first, and language feature second.

It's like saying English doesn't have a built-in politeness pronoun, so if you need to be polite you can't use English.


Not if you have no intention to do the "orientation" part of OOP.

You could always use a lang that is a Superset of C if you so badly want it for OOP purposes

Really? Linux kernel uses extensive OOP/C.

I haven't looked at how the kernel is written. But Linus has a problem using a more powerful language - if Linux adopted one just for OOP, then it will become impossible to hold back contributors from using every last feature of that language. I can understand the reluctance to deal with that.

Well he already has plenty of GCC extensions to chose from.

"If you need to do OOP in C, it's time to move to a more powerful language."

As long as is not C++...


C++ has plenty of issues, but it's really not that bad, especially modern C++. It's a huge language with tons of features, many of which you'll never use, but as far as writing C++, really not that bad. I've written a lot of C++ over the years, and I've also written a lot of vanilla C. I can tell you that you can definitely get a new project up and running much faster in C++ than in C.

I'm an OO programmer, and I avoid using classes in C++. The happy path seems to be to treat at C with STL + smart pointers, not C with classes.

About OO: Bundling data and behavior is a fine technique in some situations, nothing wrong with it. Sometimes, even inheritance makes sense.

If you are not using classes, you are not using OO at all, by definition.

That does necessarily not mean you are programming badly, although you might be. But restricting yourself to STL and smart pointers does very strongly resemble Java Disease. You might as well switch to Java.


Definition by whom, certainly not by CS and the innumerous SIGPLAN papers and variations on what OOP means since Simula came into the world.

STL has plenty of OOP concepts.

It is not that bad... Compared to c

Throwing Rust into the pool


I write Rust too. I like Rust for the mostpart, though there's some issues with the language that really wind me up, mostly to do with the unsafe/safe code boundary.

If you want full control and easier interfacing with the OS, C++ is probably a little better, but Rust is definitely a little easier to work with once you learn the language.


Meaning blood? The sharks circle.

The parent is the creator of the D language. It is a lot saner than C++ in my opinion, though it's not without its flaws

C++ is fantastic if you need a more powerful language than C because of the "You don't pay for what you don't use" philosophy.

Of course, so is D...


This to me says that C is the more powerful language. C can do what higher level languages can, in not many more lines, but higher level languages cant do the low level things C can do.

This is such a common fallacy. C exposes lower-level memory management, but to then say "oh now you have pointers you can implement anything" is a push.

If you need to implement virtual tables and function pointers, you'd use C++ - there's no need to reinvent the wheel.

Besides software engineering is about focusing on the intrinsic complexity, and using languages and tools to mitigate the incidental complexity.


I've done OOP in C, and so have others.

It's just not worth it. Too much casting, too much scaffolding, much too brittle and error-prone. The experience is like "how much suffering can one endure."


I think this approach is working too hard to realize Virtual Tables and Virtual Pointers. Another, different implementation is to define an array of function pointers, one function per method implemented, and then extend the array per subclass; each child class fills in its own or parents function pointers at init time.

Not an array, a struct is ok, and in C99 style it is very well readable.

For example Linux Kernel style is:

  static const struct file_operations fops = {
        .open    = my_open,
        .release = my_release,
        .read    = my_read,
        .write   = my_write
  };
that's all, very straightforward. Also there is no need to prefix the function name with &

PP (parent post) here - yes, several variations available in C99; I haven't tried to retrieve my examples yet. (I used this commercially in the 90s)

This pattern goes back to pre-Version-7 UNIX kernels, with only minor syntactic updates. That it long predates OO demonstrates it is not OO. That does not make it any less effective, or less useful.

It does not predate OO.

The first OO language, SIMULA-67 became public knowledge before the start of even the PDP-7 UNIX, which is extremely unlikely to have used such a pattern, which probably appeared only when UNIX was rewritten in C, starting in 1973.

Even Smalltalk-72 predated the C-version of UNIX.

However, it is likely that this pattern was chosen in UNIX independently of the previous OO languages.


Also what many usually forget, C++ was also born on the same building as UNIX.

Hence why C++ got so fast adopted across UNIXes and C compiler vendors on other platforms.


That was not the reason.

But it was the reason that C, as standardized, adopted several C++ innovations first.


It was definitly the reason, by 1992 it was impossible to buy a C compiler that also did not had a C++ one in the box.

Also why Apple moved from Object Pascal to C++ on MPW.


None of those C or C++ compilers came from Bell Labs, or were on UNIX tapes from Bell Labs. I used Cfront on Apollo machines in the '80s. Effectively nobody was using Cfront by 1992.

Apple, particularly, was not strongly influenced by Bell Labs or UNIX. Apple A/UX–a UNIX for Macintosh, a niche product from that period–shipped without a C++ compiler. MPW in 1992 shipped without a C++ compiler.


So what? Did you missed the part about compiler vendors?

Those were the ones that cared about C outside UNIX.

Bell Labs also had nothing to say to what happened to their code when they set it free.

And Zortech was one of the first vendors to move away from CFront.


To a great extent, the difference between "a data structure plus a dispatch table" and "an object" is primarily in how you squint.

It's certainly a form of polymorphism.


It's a measurable difference. OO in C++ must use double indirect vtable method calls, whilst this OO in C uses only one indirection. It's measurably faster. Objects are a bit larger though, cloning is a bit more expensive, but method calls are much faster and much easier to cache.

I think you're misinterpreting GP's code. It is a vtable, hence the static const. There would be a pointer in each struct "instance" to this static vtable, thus the same double indirection would occcur as in C++.

Plus this was a discussion about conceptual models and "what counts as OO", not the specific details and optimisation possibilities of the implementation.

OP mimic'd C++ vtables. But in C you can do better and inline them. I mostly inline them for performance reasons.

> That it long predates OO demonstrates it is not OO.

This does not make any sense. People did not wait until someone gave a name to the rule-of-three to use it, and that does not make these uses any less rule-of-three than the ones that occured post-naming.


Or perhaps go the more novel way of Piumarta's and Warth's Open, extensible object models? http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.121...

What would also be helpful is a standard way to declare to the compiler that it should or should not optimize each flagged polymorphic late bound call implementation to be early bound; moving the calls into separate or merged files during linking is not an in context solution.

not really important, retrieval of a function pointer at a known slot, and then calling that function pointer, can be really fast on a lot of arch.

I really wish people would stop reducing OOP to classes…

Classes by themselves are just another name for abstract data types, so just being able to define classes, i.e. structures together with methods that manipulate their members is not enough to qualify as OOP. Abstract data types have a field of applicability far larger than OOP.

OOP has 2 main features, which have first appeared in SIMULA-67: inheritance and virtual functions.

I agree with the following definition for OOP as as a special kind of programming with abstract data types:

"There are two features that distinguish an object-oriented language from one based on abstract data types: polymorphism caused by late-binding of procedure calls and inheritance. Polymorphism leads to the idea of using the set of messages that an object understands as its type, and inheritance leads to the idea of an abstract class. Both are important." (Ralph E. Johnson & Brian Foote, Journal of Object-Oriented Programming, June/July 1988, Volume 1, Number 2, pages 22-35)

When virtual functions are not used, the operations can be considered as belonging to the type (a.k.a. class), not to the objects, which corresponds to the jargon used by programming with abstract data types.

When virtual functions are used, the operations are considered as belonging to the objects, not to the class, as each object may have a different implementation of a given method. This corresponds with the point of view of OOP.

I have used the SIMULA/C++ term of "virtual functions", but in some OOP languages all functions are of the kind named "virtual" in SIMULA, so the "virtual" term is not used.


Well, "classes" in the mainstream programming are data types that are bound by an inheritance hierarchy. Which is exactly the problem of the "popular OOP": polymorphism (behavior) is constrained by inheritance. You cannot add a behavior to an object if that object is not part of the correct hierarchy.

Once you separate type hierarchy and behavior, you get a much more flexible system. That's what newer languages (Rust, Swift and co.) do. I think the reason why "classes" are so popular has to do with compiler technology — type hierarchies can implement polymorphism very efficiently via vtables, and folks jumped on the opportunity of having high-level abstractions with high performance (instead of doing expensive lookups associated with earlier per-instance polymorphism). But as the compiler tech and understanding of programming language theory have progressed, these limitations are not necessary for good performance anymore, in fact, they become limiting.


This is a good example of how a good programmer is more important than a a complex language with complex features. C lets you use OO if you want it ,and it doesn't require many more lines to do it, but it doesn't push you to always do it. Ive used this a number of times, but its always because its the right approach for the problem, not to be object oriented because the language is.

> C lets you use OO if you want it

Tenuous - if you are willing to implement objects yourself then you could use C sure, but I don't think that means the language "lets you use OO". It's like you could also probably implement algebraic datatypes in C using unions and structs, but would that mean "C lets you use algebraic datatypes"? I would strongly argue no.


All our languages are Turing-complete. None actively prevent any sort of programming,

Thus, to meaningfully claim that a language supports some programming technique, it has to automate it to some appreciable degree. C manifestly does not; it automates nothing but stack frames and register allocation. Every detail is hand-coded, with every opportunity to make trivial, hard-to-spot mistakes that nothing but exhaustive testing can bring to your attention.


The best OOP with C comes from a book called "Extreme C", if you're interested, check it out.


Its possible to use partial function to simulate method calls in C by memoizing the first argument which is always a pointer to the struct that method is getting called from. It requires little assembly. Check this: https://github.com/sharow/experiments/tree/master/partial

OOP in C inevitably leads to LOTS of pointer dereferencing. Pointer dereferencing hurts performance.

OOP in most languages tends to lead to lots of pointer dereferencing behind the scenes. If you're lucky and it's not much worse.

OOP in C++ leads to twice as much pointer dereferencing. Just that you don't see it doesn't mean it does not exist. Look at the generated code. It sucks.

The best thing in C++ is compile-time computation (ignoring the horrible template syntax). OO is not esp. well implemented.


Inlining the ctable into the object would only be faster if

- you have very few objects

- you have very few virtual functions

If you have a lot of objects or a lot of virtual functions, things will be less likely to fit in cache which will entirely destroy your performance, by an order of magnitude when compared to a mostly-always-branch-predicted indirection.

I remember experimenting with a an "inlined" version of std:: function and just having inlined the 5 ctor/move ctor/copy ctor/assignment operators was already slower than the vtable version when going through hundreds of callbacks ; imagine for something like Qt where QWidget or QGraphicsItem have 20+ virtual methods.


Depends. My STL in C is faster by inlining the iterator methods. With something like glib or qt double indirection would be better of course, because these can be shared then.

In my jitted ruby-like VM my methods are also copied, not referenced. Javascript also prefers copying the methods. This is usually called prototype-based OO. True prototypes copy the struct fields also, without creating classes, but copying the read-only methods only is a worthwhile OO optimization, C++ cannot do.


Can you share some benchmarks for your STL ?

Unfortunately with this approach to inheritance PIMPL goes out the window, since the class structs are now all public to facilitate embedding...

And despite this public visibility of the object's struct members granted to provide inheritance, the examples are still using getters and setters.

Ok...


Once you implement these design parameters, you've essentially written the GObject type system. Might as well use GObject at that point and get tons of language bindings for free.

("Free" after adding some metadata comments specifying parameter ownership/lifetimes, at least.)


1. Creating a struct with desired state data

2. Creating functions that take a pointer to the struct as the first parameter

3. Declaring a variable with that struct type to create an instance of the object

4. Declaring another variable with that struct type for another object instance


... is not, in fact, OO at all, by any meaningful definition. 1..4 is just programming. People have done it since long before there was any "OO" buzzword.

There are formal definitions of OO. The above satisfy exactly none of them.


There's usually a formal definition of something and a common definition of something, and most languages that get traction follow the common definition.

For example, the formal definition of the Liskov Substitution Principal:

> Let ϕ(x) be a property provable about objects x of type T. Then ϕ(y) should be true for objects y of type S where S is a subtype of T.

This doesn't allow any change in behavior when subclassing. Not even the addition of logging, which makes the overriding of methods generally useless. Yet languages still provide this feature (perhaps to their detriment, but they still do).

There's also a common definition of the Liskov Substitution Principal:

> Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

This is, generally speaking, what people are talking about when the mention the Liskov Substitution Principal, unless they're actively writing an academic paper on the topic.

Bringing up the academic definition when someone is using the common definition is 1. not relevant and 2. usually not helpful.


> This doesn't allow any change in behavior when subclassing.

Generally correct, but that's why you should only be "subclassing" abstract interfaces in the first place. Subclassing concrete object methods is a footgun, precisely because you have no way of enforcing which of these "properties" any code will actually be relying on, either at any given point in time or in the future. (Including code that's itself part of the base objects hierarchy and calling possibly-overridden methods, which means subclassing also breaks encapsulation!)

Concrete inheritance is very hard to make sense of semantically in the fully general case; if it's viable at all it is as a kind of mere specialization, and that's what the LSP is trying to get at.


I don't disagree at all. I tend to advocate for very shallow hierarchies in the 5-10% of code that I think benefits from an object oriented style.

But the point of the comment is the formal/common divide rather than the specific example.


When there are, as in the case of OO, multiple formal definitions, it is because there is no consistent "common definition" to compare any given example against, and relying on anybody's "common definition" means there is no basis for discussion.

Failing to match any of the formal definitions, as here, is a sure way to fail to reach a level where discussion is worthwhile. So, it is not, and I leave it here.


The above pattern using opaque pointers resembles objects in C++. A construct() and destruct() function returning opaque pointers (handles for the struct really) can act as custom constructors and destructors. The opaque pointer ensures a simplified API, protects internal struct data, and the defining code of struct can change without impacting the remaining source code.

This is in fact, by any meaningful definition of OOP, is a very reasonable pattern. With regards to formal definitions of OO, message passing aspect ought to be recognised rather than overemphasizing the object aspect.




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

Search: