Object-Oriented Design Patterns in C and Kernel Development
Key topics
The debate rages on: is the Linux kernel's use of function pointers in structures to achieve polymorphism truly object-oriented programming (OOP)? Some argue it's not OOP, while others point out that the definition of OOP is murky, with even pioneers like Bjarne Stroustrup and Alan Kay having differing views. As commenters dissect the technique, they reveal that similar patterns exist in other languages, with some noting that C's approach is actually similar to how many OOP languages implement polymorphism under the hood. The discussion highlights the complexity of defining OOP and the value of exploring its evolution and various interpretations.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
8h
Peak period
131
Day 2
Avg / period
26.7
Based on 160 loaded comments
Key moments
- 01Story posted
Aug 26, 2025 at 4:34 AM EDT
4 months ago
Step 01 - 02First comment
Aug 26, 2025 at 12:41 PM EDT
8h after posting
Step 02 - 03Peak activity
131 comments in Day 2
Hottest window of the conversation
Step 03 - 04Latest activity
Sep 4, 2025 at 3:50 AM EDT
4 months ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
This technique predates object oriented programming. It is called an abstract data type or data abstraction. A key difference between data abstraction and object oriented programming is that you can leave functions unimplemented in your abstract data type while OOP requires that the functions always be implemented.
The sanest way to have optional functions in object oriented programming that occurs to me would be to have an additional class for each optional function and inherit each one you implement alongside your base class via multiple inheritance. Then you would need to check at runtime whether the object is an instance of the additional class before using an optional function. With an abstract data type, you would just be do a simple NULL check to see if the function pointer is present before using it.
In the Linux VFS for example, there are optimized functions for reading and writing, but if those are not implemented, a fallback to unoptimized functions is done at the call sites. Both sets are function pointers and you only need to implement one if I recall correctly.
I think the "is/is not" question is not so clear. If you think of "is" as a whether there's a homomorphism, then it makes sense to say that it is OOP, but it can qualify as being something else too, ie. it's not an exclusionary relationaship.
If you think of the differences as being OOP implies contracts with the compiler and data abstraction does not (beyond a simple structure saying where the members are in memory), it becomes easier to see the two as different things.
You can implement OOP in C if you do vtables for inheritance hierarchies manually, among other things, but that is different than what Linux does.
It's a function belonging to an object, to which is dynamically dispatched with something I would call a vtable. To me that sounds like a classic example of OOP.
Data abstraction is a core of OOP.
This pattern can be used to implement inheritance, when it isn't here that doesn't mean its not OOP.
Sorry, but where did you got this definition from? I've always thought OOP as a way of organizing your data and your code, sometimes supported by language-specific constructs, but not necessarily.
Can you organize your data into lists, trees, and hashmaps even if your language does not have those as native types? So you can think in a OO way even if the language has no notion of objects, methods, etc.
It is from experience with object oriented languages (mainly C++ and Java). Technically, you can do everything manually, but that involves shoehorning things into the OO paradigm that do not naturally fit, like the article author did when he claimed struct file_operations was a vtable when it has ->check_flags(), which would be equivalent to a static member function in C++. That is never in a vtable.
If Al Viro were trying to restrict himself to object oriented programming, he would need to remove function pointers to what are effectively the equivalent of static member functions in C++ to turn it into a proper vtable, and handle accesses to that function through the “class”, rather than the “object”.
Of course, since he is not doing object oriented programming, placing pointers to what would be virtual member functions and static member functions into the same structure is fine. There will never be a use case where you want to inherit from a filesystem implementation’s struct file_operations, so there is no need for the decoupling that object oriented programming forces.
> I've always thought OOP as a way of organizing your data and your code, sometimes supported by language-specific constructs, but not necessarily.
It certainly can be, but it is not the only way.
> Can you organize your data into lists, trees, and hashmaps even if your language does not have those as native types?
This is an odd question. First, exactly what is a native type? If you mean primitive types, then yes. Even C++ does that. If you mean standard library compound types, again, yes. The C++ STL started as a third party library at SGI before becoming part of the C++ standard. If you mean types that you can define, then probably not without a bunch of pain, as then we are going back to the dark days of manually remembering offsets as people had to do in assembly language, although it is technically possible to do in both C and C++.
What you are asking seems to be exactly what data abstraction is, which involves making an interface that separates use and implementation, allowing different data structures to be used to organize data using the same interface. As per Wikipedia:
> For example, one could define an abstract data type called lookup table which uniquely associates keys with values, and in which values may be retrieved by specifying their corresponding keys. Such a lookup table may be implemented in various ways: as a hash table, a binary search tree, or even a simple linear list of (key:value) pairs. As far as client code is concerned, the abstract properties of the type are the same in each case.
https://en.wikipedia.org/wiki/Abstraction_(computer_science)...
Getting back to doing data structures without object oriented programming, this is often done in C using a structure definition and the CPP (C PreProcessor) via intrusive data structures. Those break encapsulation, but are great for performance since they can coalesce memory allocations and reduce pointer indirections for objects indexed by multiple structures. They also are extremely beneficial for debugging, since you can see all data structures indexing the object. Here are some of the more common examples:
https://github.com/openbsd/src/blob/master/sys/sys/queue.h
https://github.com/openbsd/src/blob/master/sys/sys/tree.h
sys/queue.h is actually part of the POSIX standard, while sys/tree.h never achieved standardization. You will find a number of libraries that implement trees like libuutil on Solaris/Illumos, glib on GNU, sys/tree.h on BSD, and others. The implementations are portable to other platforms, so you can pick the one you want and use it.
As for “hash maps” or hash tables, those tend to be more purpose built in practice to fit the data from what I have seen. However, generic implementations exist:
https://stackoverflow.com/questions/6118539/why-are-there-no...
That said, anyone using hash tables at scale should pay very close attention to how their hash function distributes keys to ensure it is as close to uniformly random as possible, or you are going to have a bad time. Most other applications would be fine using binary search trees. It probably is not a good idea to use hash tables with user controlled keys from a security perspective, since then a guy named Bob can pick keys that cause collisions to slow everything down in a DoS attack. An upgrade from binary search trees that does not risk issues from hash function collisions would be B-trees.
By the way, B-trees are containers and cannot function as intrusive data structures, so you give up some convenience when debugging if you use B-Trees.
You don't need classes for OOP. C++ not putting methods that logically operate on an object, but don't need a pointer to it, into the automatically created vtable, is an optimization and an implementation detail. I don't know why you think that putting this function into a vtable precludes OOP.
Wait, how does inheritance work when the method is not in the vtable?
As for how inheritance works when the member function is not in the vtable, that depends on what kind of member function it is. All C++ functions are given a mangled name that is stuffed into C’s infrastructure for linking symbols. For static member functions, inheritance is irrelevant since they are tied to the class. Calls to static member functions go directly to the mangled function with no indirections, just as if it had been a global function. For non-static virtual member functions, you use the vtable pointer to find it. For non-virtual member functions, the call goes straight to the function as if a global function had been called (and the this pointer is still passed, even if the function does not use it), since the compiler knows the type and thus can tell the linker to have calls there go to the function through the appropriately mangled name. It is just like calling a global function.
Yes. Since we are not in C++ we can choose to get rid of this useless pointer.
> Removing it on member functions that do not use it would pose a problem if another class inherited from this class and overrode the function definition with one that did use it.
That problem has nothing to do with the this pointer specifically. When you change the method signature of an inherited method you always have this problem. This simply means, that the superclass prescribes limits to subclasses, which is why it's possible to use a subclass inplace of a superclass.
> Maybe in very special cases whole program optimization could safely remove the this pointer, but it is questionable whether any compiler author
Yes, that's why its not done in C++, but we can do it, if we handroll it.
> it would break ABI stability
It does not if it has always been like this.
> For static member functions, inheritance is irrelevant since they are tied to the class. Calls to static member functions go directly to the mangled function with no indirections
In other words, ->check_flags() can't be implemented as a static member functions in C++. It would simply have a this pointer, that it just wouldn't use, since C++ has no way to express non-static member functions, that just don't take a this pointer.
> thus can tell the linker to have calls there go to the function
In our case the linker can only resolve the call to the appropriate vtable, since the type isn't known until runtime.
Isn't this exactly how most (every?) OOP language implements it? You would say a C++ virtual method isn't OOP?
The entire point of OOP is to make contracts with the compiler that forcibly tie certain things together that are not tied together with data abstraction. Member functions are subject to inheritance and polymorphism. Member function pointers are not. Changing the type of your class will never magically change the contents of a member function pointer, but it will change the constants of a non-virtual member function. A member function will have a this pointer to refer to the class. A member function pointer does not unless you explicitly add one (named something other than this in C++).
You claim when the compiler does this, it's OOP, but when I do it, it's not?
Of course you could implement the same in C++ and then it can't be the same as the vtable introduced by the compiler, so you would just end up with to vtables, you own and the one introduced by the compiler.
source- I wrote a windowing framework for MacOS using this pattern and others, in C with MetroWerks at the time.
As for abstract data types, they originated in Lisp, which also predates object oriented programming.
"AN ALGORITHMIC THEORY OF LANGUAGE", 1962
https://apps.dtic.mil/sti/tr/pdf/AD0296998.pdf
In this paper they are known as plexes, eventually ML and CLU will show similar approaches as well.
Only much latter would Lisps evolve from plain lists and cons cells.
https://history-computer.com/software/simula-guide/
Thus, while I had thought Lisp had ADT concepts before the first OOL existed, now I am not sure. My remark that they originated in Lisp had been said with the intention that I was talking about the first language to have it. The idea that the concept had been described outside of an actual language is tangential to what I had intended to say, which is news to me. Thanks for the link.
https://dl.acm.org/doi/pdf/10.1145/366199.366256
and the paper even starts with a critique of the efficiency of Lisp's approach for representing data with cons pairs (citing McCarthy's paper from the same year).
You might also want to watch Casey's great talk on the history of OOP
https://www.youtube.com/watch?v=wo84LFzx5nI
It's sad that OOP was corrupted by the excessively class-centric C++ and Java design patterns.
https://en.wikipedia.org/wiki/Portable_Distributed_Objects
Also dynamic runtime dispatch Smalltalk style can never be as fast as the VMT based dispatch, or compile time dispatch via generics, even with all the optimizations in place, that objc_msgSend() has had during its lifetime.
Still, Metal is implemented in Objective-C, so there is that.
That is my understanding of how the process generally works and what I am willing to guess happened. Prior to this, they had been making incremental changes to Objective-C.
That said, from what I have seen of the syntax of both languages, swift’s syntax is nicer and that is not something that they would have been able to get from Objective-C. They already had tried syntax reform for Objective-C once in the past and abandoned it.
Every few years, someone tells me I should learn another language, and in recent years, there just is no desire in my mind to want to learn yet another language that is merely another way of doing something that I already can do elsewhere and the only way I will is if I am forced (that is how I used Go).
That said, I do see what you are saying. C++ for example has an “support every paradigm” philosophy, so whenever someone who has learned C++ encounters a language using a paradigm that C++ assimilated, there is a huge temptation to try to view it through the lens of C++. I also can see the other side too: “C++ took me forever to learn. Why go through that again when I can use C++ as a shortcut to understand something else?”. C++ is essentially the Borg of programming languages.
Based on his comment, I did not think that he is proficient in them, but that he has used them, which is fair enough, so have I, sans all the ones tied to either Apple (Swift) or Microsoft (C#).
I have some projects in Haskell just for curiosity's sake, and because what I wanted seemed like it would be nice in Haskell, and it indeed looks quite elegant to me, for this one particular project. Haskell is not a language I would use generally. OCaml is.
There are some languages in which I am extremely proficient. My best language is C, which is my favorite and I have used most features of every version of C from C89 to C11. My second best is probably either C++ or POSIX shell (although I have moments where I forget certain syntax and need to look it up, especially in POSIX shell for variants on variable substitution, e.g. ${VAR%%foobar}). I have used most features of C++98 and some from newer versions. My experiences with C++ have soured me on it, so I now try to avoid C++ whenever I can in favor of C and Python.
My first language was actually PHP 4.2.y, and I was fairly proficient in it, having spent a long time learning it while simultaneously writing my own code for a website as a teenager. However, I never once touched the portions describing objects/classes, namespaces or exceptions. Someone else at work writes Modern PHP code using Symfony and I have taken a peek at it. It looks very different from the PHP I knew because it uses the features I had avoided learning (and probably some new language features too), although I can sort of read it thanks to having learned those concepts in other languages.
I used SML/NJ and Java in college. Years after college, I modified an open source Android TV application written in Java to add some things I wanted, although honestly, beyond that I have not really touched either language. Give me an arbitrary application written in either to improve and I would have some difficulty, although I will probably be able to do it after filling the gaps in my understanding (and doing plenty of head banging if it is a large program).
I have used JavaScript for a few recent projects via electron/nodejs and I have done several small things in Python over the past several years. Each time, I only worked with the subset that I need. I am far from a master of either language that can understand arbitrary code written in them, but I am able to manage as far as my specific needs are concerned.
I could continue listing my experiences doing things in languages (like the time in college that I wrote some basic programs in FORTRAN 90 to try to learn it), but they really are not that interesting. It was often a project here or a small application there, and as I readily admitted, I only used a subset of most of the languages. For programming, a subset of commonly used bits is often all you need.
This introduces performance issues larger than the typical ones associated with vtable lookups. Not all domains can afford this today and even fewer in the 80s/90s when these languages were first designed.
> It's sad that OOP was corrupted by the excessively class-centric C++ and Java design patterns.
Both Smalltalk and Objective-C are class based and messages are single-receiver dispatched. So it’s not classes that you’re objecting to. It’s compile-time resolved (eg: vtable) method dispatch vs a more dynamic dispatch with messages.
Ruby, Python, and Javascript all allow for last-resort attribute/message dispatching in various ways: Ruby via `method_missing`, Python by supplying `__getattr__`, and Javascript via Proxy objects.
We are talking about an optimization of a language implementation here. This would be very much written in a ASM or another language were this is defined.
Don't know about other programming languages but with Objective-C due to IMP caching the performance is close to C++ vtable
https://mikeash.com/pyblog/friday-qa-2016-04-15-performance-...In Smalltalk systems that stop being an issue after JITs got introduced.
Often class-based programming is confused as being the only style of OOP, superior to all other styles, or heavy-handedly pushed on others. Many programmers are perfectly fine with using objects or only specific features of OOP, without classes, if they are "allowed" to.
smalltalk is not original OO - c++ took oo from simula which was always a different system.
I would rather say that OOP is a formalization of predating patterns and paradigma.
For full disclosure, I have never verified that leaving ->read() unimplemented when ->read_iter() is implemented is safe, but I have seen enough examples of code that I strongly suspect it is and if it is not, it is probably a bug.
I think this is more of an effect of C distinguishing between allocating memory (aka object creation) and initialization, which other languages disallow for other reasons, not because there are not OOPy enough.
So that is to say, if the base class has a certain function which must be implemented and must provide certain behaviors, then the derived class must implement that function and provide all those behaviors.
The POSIX functions like read and write do not have a contract which says that all implementations of them must successfully transfer data. Being unimplemented (e.g returning -1 with errno EOPNOTSUPP or whatever) is allowed in the contract.
OOP just wants a derived thing to obey the contract of the abtraction it is inheriting, so if you want certain liberties, you have to push them into the contract.
As far as OOP is concerned, lack of implementation is not an issue in instantiating something - an object will just not understand the message, possibly catastrophically.
This usually resolves to a function call because it's the easiest and most sensible way to do it.
Objective-C is more explicit about it due to Smalltalk heritage. Some languages model objects as functions (closures) that one calls with the "message" (an old trick in various FP languages is to implement a trivial object system with closures for local state).
[1] Arguably CLOS with Generic Functions can be seen as outlier, because the operation becomes centerpiece, not the object.
The LSP is never absolute in a practical system. Because why would you have, say, in a casee of inheritance, a Y which is an new kind of X, if all it did was substitute for X, and behave exactly the same way? Just use X and don't create Y; why create a substitute that is perfectly identical.
If there is a reason to introduce a new type Y which can be used where X is currently used, then it means they are not substitutable. The new situations need a Y and not X, and some situations don't need a Y and stay with X.
In a graphics program, an ellipse and rectangle are substitutable in that they have a draw() method and others, so that they plug into the framework.
But they are not substitutable in the user's design; where the user needs an ellipse, a rectangle won't do, and vice versa.
In that case the contract only cares about the mechanics of the program: making multiple shapes available in a uniform way, with a program organization that makes it easy to code a new shape.
The substitution principle serves the program organization, and not much more.
So with the above discussion in place, we can make an analogy: an ellipse object in a vector graphics program can easily be regarded as a version of a rectangle with unimplemented corners.
The user not onnly doesn't mind that the corners are not implemented, but doesn't want them because they wouldn't make sense.
In the same way, it doesn't make sense to have lseek() on a pipe, or accept() on TTY descriptor or write() on a file which is read only to the caller, etc.
LSP is about behaviour existing in the supertype. Adding behaviour doesn't violate LSP.
> In a graphics program, an ellipse and rectangle are substitutable in that they have a draw() method and others, so that they plug into the framework.
The behaviour in question means it draws something. It can draw something different every time, and not violate LSP here.
class DefaultTask { }
class SpecialTask { }
class UsedItem {
}Is python a OOP language? Self / this / object pointer has to be passed similar to using C style object-oriented / data abstraction.
When a method is never overridden, it doesn't need to be in the vtable.
I think composition over inheritance is only about being explicit. That's it.
The difference is that design patterns are a technique where you use features not implemented by the compiler or language, and all the checks have to be done by the developer, manually.
Thus, you are doing part of the work of the compiler.
In assembler, a function call is a design pattern.
(sorry it took more than a decade for Java to catch up and Sun Microsystems originally sued Microsoft for trying to add lambdas to java way back when, and even wrote a white paper insisting that anonymous inner classes are a perfectly good substitute - stop laughing)
I personally don't like implicit this. You are very much passing a this instance around, as opposed to a class method. Also explicit this eliminates the problem, that you don't know if the variable is an instance variable or a global/from somewhere else.
People typically use some kind of naming convention for their member variables, e.g. mFoo, m_Foo, m_foo, foo_, etc., so that's not an issue. I find `foo_` much more concise than `this->foo`. Also note that you can use explicity this in C++ if you really want to.
In the first example, object1 and object2 can very much be of the same type or compatible types/subtypes/supertypes. Having vtables per object as opposed to per class to me indicates, that it IS intended to modify the behaviour of an object by changing it's vtable. Using the behaviour of another object of the same type to treat the second object, seams valid to me.
In the second case, it's not about the child implementation dispatching to the superclass, it's about some external code wanting to treat it as an object of the supertype. It's what in other languages needs an upcast. And the supertype might also have dynamic behaviour, otherwise you of course wouldn't use a vtable.
Upcasting is fine, but generally speaking the expected behavior of invoking a superclass method on an object that is actually a subclass is that the subclass method implementation is used (in C++, this would be a virtual/override type method, as opposed to a static method). Invoking a superclass-specific method impl on a subclass object is kind of weird.
In C you can also change the "class" of an instance as needed, without special syntax. Maybe you need to already call a method of the new/old class, before/after actually changing the class type.
> is that the subclass method implementation is used
The entire point of invoking the superclass method is, because the subclass has a different implementation and you want to use the superclass implementation.
Maybe it's a bit due to its evolution. It started with a language that should have all features every possible, that was to complicated to be implemented at the time. Then it was dumbed down to a really simple language. And then it evolved along side a project adding the features, that are truly useful.
error = old_dir->i_op->rename(rd->new_mnt_idmap, old_dir, old_dentry, new_dir, new_dentry, flags);
https://github.com/torvalds/linux/blob/master/fs/namei.c#L51...
That is a close match for the first example, with additional arguments.
It is helpful to remember that this is not object oriented programming and not try to shoehorn this into the paradigm of object oriented programming. This is data abstraction, which has similarities (and inspired OOP), but is subtly different. Data abstraction does not automatically imply any sort of inheritance. Thus you cannot treat things as necessarily having a subclass and superclass. If you must think of it in OOP terms, imagine that your superclass is an abstract class, with no implemented members, except you can instantiate a child class that is also abstract, and you will never do any inheritance on the so called child class.
Now, it is possible to implement things in such a way where they actually do have something that resembles a subclass and a superclass. This is often done in filesystem inode structures. The filesystem will have its own specialized inode structure where the generic VFS inode structure is the first member and thus you can cast safely from the generic inode structure to the specialized one. There is no need to cast in the other direction since you can access all of the generic inode structure’s members. This trick is useful when the VFS calls us via inode operations. We know that the inode pointer is really a pointer to our specialized inode structure, so we can safely cast to it to access the specialized fields. This is essentially `superclass->op->start(object)`, which was the second example.
Data abstraction is a really powerful technique and honestly, object oriented programming rarely does anything that makes me want it over data abstraction. The only thing that I have seen object oriented programming do better in practice than data abstraction is marketing. The second example is similar to C++’s curiously recurring template pattern, which adds boilerplate and fighting with the compiler with absurdly long error messages due to absurdly verbose types to achieve a result that often at best is the same thing. On top of those headaches, all of the language complexity makes the compile times slow. Only marketing could convince someone that the C++ OOP way is better.
https://www.kernel.org/doc/html/latest/filesystems/vfs.html
As I said previously, it is helpful to remember that this is not object oriented programming and not try to shoehorn this into the paradigm of object oriented programming. Calling this a vtable is wrong.
That said, similar macro magic is used in C generic data structures and it works very well.
> It could also contain variables that are non-pointers.
The convention of it being a pure vtable is that it just doesn't.
> Neither is allowed in a vtable
Who is the vtable membership authority? :-)
When its a table of function pointers used for dynamic dispatch, to me it's a vtable. I don't care about their type signatures as long as they logically belong to the object in question.
You seem to have a different very narrow definition of vtables, so the discussion is kind of useless.
That said, I like having a this pointer explicitly passed as it is in C with ADTs. The functions that do not need a this pointer never accidentally have it passed from the developer forgetting to mark the function static or not wanting to rewrite all of the function accesses to use the :: operator.
If all you see in code is a very tiny 3 character expression, you won't be able to make much of a judgement about it to begin with.
What I don’t like is being able to reference instance members without `this`, e.g.
If it was this->bar it could be a member, it could also be a static variable. A bar on its own could be local or it could be in any of the enclosing scopes or namespaces. Forcing "this" to be explicit doesn't make the code any clearer on its own.
I wouldn't be surprised if any null check against this would be erased by the optimizer for example as the parent comment mentioned. Sanitizers might check for null this too.
That would be a complete redability disaster... at least for C++. Java peeps probably won't even flinch ;)
For the function naming, nothing stops you from doing the same in C:
That doesn't stop you from mentioning s twice. While it is redundant in the common case, it isn't in every case like I wrote elsewhere. Also this is easily fixable as written several times here, by a macro, or by using the type directly.Ask how do I do this, well see it's magic. It just happens.
Something went wrong? That's also magic.
After 40 years I hate magic.
It would be nice though, if syntax like the following would be supported:
Of course casting to a subclass isn't guaranteed to succeed always, but for example when you have actually declared it as the subclass elsewhere it's fine without checking for isinstance.
There is a saying “If Your Only Tool Is a Hammer Then Every Problem Looks Like a Nail”. That is precisely what is happening here with the insistence to call what appears to be all structures of function pointers vtables. A vtable is something that follows a fairly well defined pattern for implementing inheritance. Not all things containing function pointers are vtables.
That's nice, but the entire point is, that the caller doesn't know the type of the object, it only has a supertype. That's why you need dynamic dispatch here. Of course you can implement dynamic dispatch without function pointers, but it is done with function pointers here. If you don't want to name dynamic dispatch implemented with function pointers a vtable, OK, that's fine, but that's the definition I am familiar with.
Read my previous comment for the bug that would happen if what us being used in Linux were actually used for dynamic dispatch when implementing inheritance, and it should be clear this is something similar, but different.
I wrote about this concept[1] for my own understanding as well -- just tracing the an instance of the pattern through the tmux code.
[0] https://raw.githubusercontent.com/tmux/tmux/1536b7e206e51488... [1] https://blog.drnll.com/tmux-obj-oriented-commands
60 more comments available on Hacker News