> The article describes how the Linux kernel, despite being written in C, embraces object-oriented principles by using function pointers in structures to achieve polymorphism.
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.
I largely agree, and use these patterns in C, but you’re neglecting the usual approach of having a default or stub implementation in the base for classic OOP. There’s also the option of using interfaces in more modern OOP or concept-style languages where you can cast to an interface type to only require the subset of the API you actually need to call. Go is a good example of this, in fact doing the lookup at runtime from effectively a table of function pointers like this.
The concept of abstract data type is a real idea in the days of compiler design. You might as well say "compiler design predates object oriented programming". The technique described in the lead is used to implement object-oriented programming structures, just as it says. So are lots of compiler design features under the hood.
source- I wrote a windowing framework for MacOS using this pattern and others, in C with MetroWerks at the time.
> In Smalltalk and Objective-C, you just check at runtime whether an object instance responds to a message. This is the original OOP way.
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.
This is a very good point. A greater distinction needs to be made about class-based versus class-free OOP. Many of the problems or constraints around OOP that people don't like, come back to classes.
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.
OOP cannot be a formalization of what predated it because the predating patterns support things that OOP explicitly disallows, like instantiation with unimplemented functions. That is extremely useful when you want to implement an optional function, or mutually exclusive functions such that you pick which is optional. This should be the case in the Linux VFS with ->read() and ->read_iter(). Also, ASTs were formalized after OOP, despite existing prior to it in Lisp.
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.
You can do exactly what was done in C with most OOP languages like Java & C# because you have lambdas now, and lambdas are just function pointers. You can literally assign them to instance variables (or static variables).
(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)
An abstract data type is a software design pattern.
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.
> Having to pass the object explicitly every time feels clunky, especially compared to C++ where this is implicit.
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.
“this” is a reserved keyword in C++, so you do not need to worry about it being a global variable.
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.
Agreed, one of the biggest design mistakes in the OOP syntax of C++ (and Java, for that matter) was not making `this` mandatory when referring to instance members.
I don't quite agree, especially because the implicit this not only saves you from explicitly typing it, but also because by having actual methods you don't need to add the struct suffix to every function.
Note that this is using interfaces (i.e. vtables, records of function pointers), not full object-orientation. Other OO features, like classes and inheritance, have much more baggage, and are often not worth the associated pain.
vtables contain function pointers to functions that take “this” pointers. The author mentions struct file_operations as an example of a vtable. struct file_operations contains a pointer to a function that does not take “this” pointer. It is not even a vtable.
Yup. I've often wonder why the aversion to C++ since they are obviously using objects. Is it that they don't want to also enable all the C++ language junk like templates or OO junk like inheritance?
I always wonder, why not anything similar made it into a new (some) C version? Clearly, there is a significant demand for - lots of people reimplementing the same (similar) set of patterns.
Another cool thing about this approach is you can have the arguments to your object init be a pointer to a structure of args. Then down the line you can add features to your object without having to change all the calls to init your object throughout the code base.
I've done this on a few smaller projects when I was in college. It's fun bringing something similar to OOP into C; however you can get into trouble really quickly if you are not careful.
If this is the pattern you prefer, why not choose a language that caters to it? Choosing C just seems like you're TRYING to shoot yourself. I don't care how good you are at coding, this is just a bad decision.
I was involved in a product with a large codebase structured like this and it was a maintainability nightmare with no upsides.
Multiple attempts were made to move away from this to no avail.
Consider that the code has terrible readability due to no syntax-sugar, the compiler cannot see through the pointers to optimise anything, tooling has no clue what to do with it.
On top of that, the syntax is odd and requires any newbies to effectively understand how a c++ compiler works under-the-hood to get anything out of it.
On top of those points, the dubious benefits of OOP make doing this a quick way to kill long-term maintainability of your project.
For the devs who come after you, dont try to turn C into a poor-mans C++.
If you really want to, please just use C++.
Can you elaborate what exactly the maintainability nightmare was?
To me less syntactic sugar is more readable, because you see what function call involves dynamic dispatch and which doesn't. Ideally it should also lead to dynamic dispatch being restricted to where it is needed.
I don't know where (might also have been LWN), but there was a post about it actually being more optimizable by the compiler, because dynamic code in C involves much less function pointers and the compiler can assume UB more often, because the assignments are in user code.
> requires any newbies to effectively understand how a c++ compiler
You are not supposed to reimplement a C++ compiler exactly, you are supposed to understand how OOP works and then this emerges naturally.
> dont try to turn C into a poor-mans C++
It's not poor-mans C++, when it's idiomatic C.
People like me very much choose C while having this usage in mind, because its clearer and I can sprinkle dynamism where it's needed not where the language/compiler prescribes it and because every dynamism is clear because there is not dynamic sugar, so you can't hide it.
Hi, I don't know much about this. But it seems to me that the OP is doing it differently than the kernel devs. If you read the article that the OP links, then you get the impression that the vtables contain typed function pointers, while OP uses void pointers. Also the main benefit mentioned in the kernel dev article is that you save memory, by not having multiple function pointers in each structure instance, but instead you have just one pointer to a vtable in each instance. Thus the main benefit is saving memory according to kernel dev, but OP uses this vtable as a form of indirection to implement runtime method swapping and polymorphism, which is not even mentioned in the kernel dev article. Thus, OP uses some other pattern than the one mentioned by kernel dev.
OP doesn't use void pointers, he uses void. He writes about functions having no arguments and returning nothing for the same reason other blog posts name functions foo and bar.
> OP uses this vtable as a form of indirection to implement runtime method swapping and polymorphism
The kernel uses vtables to implement polymorphism, it doesn't store the vtable in the object to save space. If there is no polymorphism, you don't use a vtable at all, that's saving even more space.
> If there is no polymorphism, you don't use a vtable at all, that's saving even more space.
Not true. You use a vtable even if there is no polymorphism, in cases where you want to have objects store pointers to their methods (OO interface), but don't want each instance to have pointers to all methods. I was referring to this article, which the OP links in his post https://lwn.net/Articles/444910/
When do objects need to have function pointers? If the function never changes per object, then you would just directly call the function. When do you use function pointers? When the caller only knows the type of struct foo_ops, but not the actual called function. That's polymorphism.
You can redefine the methods at runtime. If for example I have a logger that logs on the local drive and another one that logs on a networked drive I can swap the method pointer. This is not polymorphism since the object does have to change. I can also load the function from a shared library that is unknown during compilation and use that as part of the objects methods.
I would call that polymorphism, since the object type the caller sees, still stays the same, i.e. the vtable itself or some containing struct/class. So as far as the caller is concerned, the object he has access to behaves differently.
41 comments
[ 2.9 ms ] story [ 67.5 ms ] threadThis 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.
source- I wrote a windowing framework for MacOS using this pattern and others, in C with MetroWerks at the time.
It's sad that OOP was corrupted by the excessively class-centric C++ and Java design patterns.
smalltalk is not original OO - c++ took oo from simula which was always a different system.
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.
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.
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.
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.
(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)
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.
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.
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.
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.
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
No need to pass in the object explicitly, etc.
Doesn't have the greatest documentation, but has a full test suite (e.g., [1][2]).
[0] https://github.com/peterpaul/co2
[1] https://github.com/peterpaul/co2/blob/master/carbon/test/pas...
[2] https://github.com/peterpaul/co2/blob/master/carbon/test/pas...
I was involved in a product with a large codebase structured like this and it was a maintainability nightmare with no upsides. Multiple attempts were made to move away from this to no avail.
Consider that the code has terrible readability due to no syntax-sugar, the compiler cannot see through the pointers to optimise anything, tooling has no clue what to do with it. On top of that, the syntax is odd and requires any newbies to effectively understand how a c++ compiler works under-the-hood to get anything out of it.
On top of those points, the dubious benefits of OOP make doing this a quick way to kill long-term maintainability of your project.
For the devs who come after you, dont try to turn C into a poor-mans C++. If you really want to, please just use C++.
To me less syntactic sugar is more readable, because you see what function call involves dynamic dispatch and which doesn't. Ideally it should also lead to dynamic dispatch being restricted to where it is needed.
I don't know where (might also have been LWN), but there was a post about it actually being more optimizable by the compiler, because dynamic code in C involves much less function pointers and the compiler can assume UB more often, because the assignments are in user code.
> requires any newbies to effectively understand how a c++ compiler
You are not supposed to reimplement a C++ compiler exactly, you are supposed to understand how OOP works and then this emerges naturally.
> dont try to turn C into a poor-mans C++
It's not poor-mans C++, when it's idiomatic C.
People like me very much choose C while having this usage in mind, because its clearer and I can sprinkle dynamism where it's needed not where the language/compiler prescribes it and because every dynamism is clear because there is not dynamic sugar, so you can't hide it.
OP doesn't use void pointers, he uses void. He writes about functions having no arguments and returning nothing for the same reason other blog posts name functions foo and bar.
> OP uses this vtable as a form of indirection to implement runtime method swapping and polymorphism
The kernel uses vtables to implement polymorphism, it doesn't store the vtable in the object to save space. If there is no polymorphism, you don't use a vtable at all, that's saving even more space.
Not true. You use a vtable even if there is no polymorphism, in cases where you want to have objects store pointers to their methods (OO interface), but don't want each instance to have pointers to all methods. I was referring to this article, which the OP links in his post https://lwn.net/Articles/444910/
You can redefine the methods at runtime. If for example I have a logger that logs on the local drive and another one that logs on a networked drive I can swap the method pointer. This is not polymorphism since the object does have to change. I can also load the function from a shared library that is unknown during compilation and use that as part of the objects methods.