31 ms·
Object-oriented design patterns in C and kernel development
- 1718627440 1y ago> 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.
- spacechild1 1y ago> 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.
- 1718627440 1y agoIn code I write, I can know what variables mean. The feature loses its point, when it's not mandatory. Also being explicit allows you to be more expressive with variable name and ordering.
- elteto 1y ago...and C++ added explicit this parameters (deducing this) in C++23.
- loeg 1y agoI think the author is talking about this: object->ops->start(object) Where not only is it explicit, but you need to specify the object twice (once to resolve the Vtable, and a second time to pass the object to the stateless C method implementation).
- 1718627440 1y agoYes I know. From the caller that might seem to be redundant, my argument was about the callee's side. Also it is not truely redundant, as you can write: object1->op->start(object2) superclass->op->start(object)
- loeg 1y agoI think both of these invocations are invalid. Using object1's vtable methods on object2, obviously, but in the latter case: the vtable method should just point at the superclass impl, if not overridden. And if overridden and the child impl needs to call the superclass, it can just do so without dispatching through some vtable.
- 1718627440 1y agoWhen you think of vtables as unique or owned by an object, then these example seem weird to you. When you think of them as orthogonal to your types/objects, these examples can be useful. 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.
- loeg 1y agoI think it is wrong/weird for objects of the same type to have different vtables, yes. I would call those different types. 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.
- ryao 1y ago“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.
- wmanley 1y agoIt’s not about ‘this’ being a global, it’s if you see ‘i++’ in code it’s not obvious if ‘i’ is a member or not without having to check context.
- Kranar 1y agoIf you see "i++" in code and you don't have any context about what "i" is, then what difference does it make if "i" is a member variable, global variable, parameter, etc etc... 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.
- ryao 1y agoNot allowing a variable to implicitly refer to a member variable makes it much easier to find. If it is not declared in the function and there is no implicit dereferencing of a this pointer, the variable is global. If the variable name is commonly used and it is a member variable, it is a nightmare to hunt for the correct declaration in the codebase with cscope.
- ryao 1y agoGood point. I had misunderstood the previous comment as suggesting that this be passed to the member function as an explicit argument, rather than requiring dereferences of this be explicit. The latter makes far more sense and I agree it makes reasoning about things much easier.
- ActorNightly 1y agoYou can also get clever with macros.
- MontyCarloHall 1y agoAgreed, 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.
- chuckadams 1y agoC++ and Java went for the "objects as static closures" route, where it doesn't make any sense to have a `this`. Or, they made them superficially look like static closures, which in hindsight was probably not the best idea. Anyway, Java lets you use explicit `this`, I don't recall whether C++ makes it into a footgun or not.
- MontyCarloHall 1y agoBoth languages let you use explicit `this` but don’t mandate it. The “static closure” approach is great. I don’t like having to explicitly pass `this` as a parameter to every method call as in the OP (or worse, the confusing Python approach of forcing `self` to be explicitly written in every non-static method signature but having it be implicitly passed during method calls). What I don’t like is being able to reference instance members without `this`, e.g. void foo() { int x = bar + 1; // should be illegal: it can be hard to distinguish if `bar` is a local variable versus an instance member int y = this->bar + 1; // disambiguation is good }
- josefx 1y ago> int x = bar + 1; // should be illegal: it can be hard to distinguish if `bar` is a local variable versus an instance member 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.
- ryao 1y agoThe guy was referring to the explicit case where bar is a member variable. The cases where it is in the local scope under scoping rules or the global scope are not really an issue, since you can check the function definition to find if it is local. In the case that it is not in the function definition, then it is in the global scope in C. If implicit this were not done in C++, that would also be the case in C++, provided you do the sane thing and use the std namespace for everything. Just thinking about the headaches namespaces could cause when looking for such definitions with cscope gives me yet another reason to stay away from C++ whenever possible.
- Gibbon1 1y agoThe implicit this sounds to me like magic. Magic! 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.
- Galanwe 1y agoI 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. mystruct_dosmth(s); mystruct_dosmthelse(s); vs s->dosmth(); s->dosmthelse();
- 1718627440 1y agoMy problem with implicit this is more, that you can access member variables, without it being explicit, i.e. about the callee, not about the caller. For the function naming, nothing stops you from doing the same in C: static dosmth (struct * s); s->dosmth = dosmth; 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.
- Galanwe 1y agoThis is not the same, you introduced dynamic function resolution (i.e.a function pointer tied to a specific instance), we are talking about static function resolution (purely based on the declared type).
- 1718627440 1y agoTrue, if you don't trust the compiler to optimize that, then you must live with the C naming.
- tdrnl 1y agoA talk[0] about Tmux is where I learned about this pattern in C. 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/1536b7e206e51488c37379df22b8c58ef3febc28/presentations/tmux_asiabsdcon11.pdf https://raw.githubusercontent.com/tmux/tmux/1536b7e206e51488... [1] https://blog.drnll.com/tmux-obj-oriented-commands https://blog.drnll.com/tmux-obj-oriented-commands
- munchler 1y agoNote 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.
- PhilipRoman 1y agoField inheritance is surprisingly natural in C, where a struct can be cast to it's first member.
- 1718627440 1y agoNote that you only need to cast for an upcast. To access the first member, you wouldn't need to cast. It would be nice though, if syntax like the following would be supported: struct A { int a; }; struct B { int b; struct A a; }; void foo (struct A * a) { struct B * b; &b->a = pa; } struct B b; foo (&b.a);
- PhilipRoman 1y agoYeah you're right, I meant the other way around. Also another loosely related idea is the container_of macro in Linux kernel.
- 1718627440 1y agoYeah, my idea is literally native type-safe support of container_of for assignment in the compiler.
- teo_zero 1y agoIn what scenario would this be useful? If foo() takes a struct A, it should be more generic and have no knowledge about the more specialized struct B.
- 1y ago
- 2OEH8eoCRo0 1y agoYup. 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?
- nphardon 1y agoHere's one example. For us, it's more a tradeoff rather than an aversion. There's pros (manual memory management in C) and cons (manual memory management in C) for each. We do math operations (dense and sparse matrix math for setting up and solving massive systems of differential equations) on massive graphs with up to billions of nodes and edges. We use C in parts of the engine because we need to manage memory at a very fine level to meet performance demands on our tool. Other parts of the tool use C++ because they decided the tradeoff benefited in the other direction, re memory access / management / ease of use. As a result we need really robust qa around memory leaks etc. and tbh we rely on one generational talent of an engineer to keep things from falling apart; but we get that speed. As a side note, we implement objects in C a little more complex than the op, so that the object really does end up as a black box to the user (other engineers), with all the beauty of data agnosticism.
- TuxSH 1y agoWhat parts of it can't just be compiled as C++ code? (unless it has to do with the subtle difference in implicit lifetime object rules) IMO it's much easier to write resleaks/double-frees with refcounted objects in C than it is in C++
- 1718627440 1y agoVLAs, named structure assignment, sane treatment of the void type, having different "lifetimes" for object (the C understanding of object) existence and initialization, having different namespaces for composed types and variables, a local error handling convention, leading to better error messages, robuster behaviour and a feeling for completeness, a lot of examples of the article and here in the thread, and most importantly no magic.
- pakl 1y agoA few years ago Peterpaul developed a lightweight object-oriented system on top of C that was really pleasant to use[0]. 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 https://github.com/peterpaul/co2 [1] https://github.com/peterpaul/co2/blob/master/carbon/test/pass/Object.test https://github.com/peterpaul/co2/blob/master/carbon/test/pas... [2] https://github.com/peterpaul/co2/blob/master/carbon/test/pass/class_decl_inheritance.test https://github.com/peterpaul/co2/blob/master/carbon/test/pas...
- guerrilla 1y agoFor people wondering what it looks like without the syntactic sugar of carbon then look here [0]. As far as I can see, there's no support for parametric polymorphism. 0. https://github.com/peterpaul/co2/tree/master/examples/my-object/src https://github.com/peterpaul/co2/tree/master/examples/my-obj...
- 1718627440 1y agoDoesn't look much different than GLib the base for the GTK implementation (and other things in GNOME, the GNU Network _Object_ Model Environment).
- cryptonector 1y agoObjects yes, classes and inheritance no. Just interfaces please.
- saagarjha 1y agoI feel like Vala tries to fit in this niche too.
- BinaryIgor 1y agoI 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.
- davikr 1y agoProbably into the High C Compiler.
- 1718627440 1y agoWhenever you invent syntactic sugar you need to make some usage blessed and some usage impossible/needing to fallback to the old way without syntactic sugar. See https://news.ycombinator.com/item?id=45040662 https://news.ycombinator.com/item?id=45040662. Also some point of C is, that it doesn't hide that dynamic complexity. You always see when there is dynamic dispatch. There are tons of language, which introduce some formalism for these concepts, honestly most modern imperative languages seem to be. The unique selling point of C is, that you see the complexity. That influences you to only use it if you really want it. Also the syntax isn't really that complicated.
- nphardon 1y agoAnother 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.
- accelbred 1y agoI usually put an inline wrapper around vtable functions so that `thing->vtable->foo(thing, ...)` becomes `foo(thing, ...)`.
- SLWW 1y agoI'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.
- deleted 1y ago[deleted]
- MangoToupe 1y agoIf 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.
- 1718627440 1y agoBecause they like how C caters to this. This question was asked here several times, please read the answers there.
- MangoToupe 1y agoOk so... why choose C if they know they're shooting themselves?
- 1718627440 1y ago> Ok so... why choose C if they know they're shooting themselves? > Because they like how C caters to this. We(aka I) think we are shooting ourselves less, because C represents the algorithms more in a way how we want to express them. C's lack of syntactic sugar means dynamic dispatch is always visible. C not prescribing which function pointers you can use, means that the most fitting way can be chosen as described by the article and the LWN post, as opposed to shoehorning it into some paradigm prescribed by the language, which causes more problems done the line.
- TickleSteve 1y agoNever. Do. This... 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++.
- 1718627440 1y agoCan 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.
- wosined 1y agoHi, 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.
- 1718627440 1y ago> while OP use void pointers 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.
- wosined 1y ago> 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/ https://lwn.net/Articles/444910/
- 1718627440 1y agoWhen 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.
- ryao 1y ago> 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.
- mistrial9 1y agoThe 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.
- ryao 1y agoCompiler design does predate object oriented programming. The first compiler was made by John Backus et al at IBM in April 1957. As for abstract data types, they originated in Lisp, which also predates object oriented programming.
- pjmlp 1y agoActually, no. "AN ALGORITHMIC THEORY OF LANGUAGE", 1962 https://apps.dtic.mil/sti/tr/pdf/AD0296998.pdf 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.