12 comments

  • pakl 9 hours ago
    A 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

    [1] https://github.com/peterpaul/co2/blob/master/carbon/test/pas...

    [2] https://github.com/peterpaul/co2/blob/master/carbon/test/pas...

    • guerrilla 7 hours ago
      For 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-obj...

      • 1718627440 6 hours ago
        Doesn't look much different than GLib the base for the GTK implementation (and other things in GNOME, the GNU Network _Object_ Model Environment).
    • saagarjha 46 minutes ago
      I feel like Vala tries to fit in this niche too.
  • 1718627440 12 hours 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.

    • MontyCarloHall 6 hours ago
      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.
      • chuckadams 5 hours ago
        C++ 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 5 hours ago
          Both 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 4 hours 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.

        • cherryteastain 4 hours ago
          this in C++ is just a regular pointer, it has no special footguns, just the typical ones you have with pointers in general
          • jcelerier 4 hours ago
            that's not really true - unlike a regular pointer, `this` is not allowed to be null, thus removing `if(this == nullptr)` is always a valid optimization to do.
            • cherryteastain 2 hours ago
              It absolutely is allowed to be null:

                  #include <iostream>
                  struct Foo {
                    void bar() {
                      std::cout << this << std::endl;
                    }
                  };
                  
                  int main() {
                    Foo *p = nullptr;
                    p->bar();
                  }
              
              will print 0
              • steveklabnik 1 minute ago
                Just to cite what the others have told you: https://godbolt.org/z/bWfaYrqoY

                    /app/example.cpp:10:6: runtime error: member call on null pointer of type 'Foo'
                    SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /app/example.cpp:10:6 
                    /app/example.cpp:3:8: runtime error: member call on null pointer of type 'Foo *'
                    SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /app/example.cpp:3:8
              • Krssst 2 hours ago
                This is undefined behavior in my understanding, it just happens to work until it doesn't.

                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.

              • usefulcat 1 hour ago

                    Foo *p = nullptr;
                    p->bar();
                
                That's undefined behavior. It's "allowed" in the sense of "yes it's possible to write, compile and run that code", but the language makes no guarantees about what the results will be. So maybe not the most useful definition of "allowed".
            • hhdknddkkjd 3 hours ago
              Only in so much that this being null is UB, but in the real world it very much can be null and programs will sometimes crash deep inside methods called on nullptr.
    • loeg 11 hours ago
      I 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).
      • ginko 8 hours ago
        Nothing a little macro magic couldn't fix..

          #define CALL(object, function, ...) (object->ops->function(object, __VA_ARGS__))
        • ActorNightly 2 hours ago
          You can also just replicate the vtable of sorts in C that keeps track of things when new objects are created.
        • loeg 1 hour ago
          You can just use C++ instead of reinventing a worse version of C++98!
      • 1718627440 10 hours ago
        Yes 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 10 hours ago
          I 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 9 hours ago
            When 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 9 hours ago
              I 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.

              • 1718627440 8 hours ago
                In most languages, this is not possible, because they abstract over the implementation of classes. In C it is so you can be more creative. You can for example use it instead of a flag for behaviour. Why branch on a variable and then call separate methods, when you can simply assign the wanted implementation directly. If you want to know, which implementation/mode is used, comparing function pointers and scalar variables amounts to the same. It is also an easy way to get a unique number. When all implementations of that can operate on the same type, they are interchangeable.

                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.

            • zozbot234 7 hours ago
              "Orthogonal" vtables are essentially traits/typeclasses.
              • 1718627440 7 hours ago
                What I really like about C is that it supports these sophisticated concepts without having explicit support for them. It just naturally emerges from the core concepts. This is what makes it feel like it just doesn't restrict the programmer much.

                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.

          • ryao 8 hours ago
            You are wrong about those invocations being invalid. Such patterns happen in filesystem code fairly often. The best example off the top of my head is:

            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.

            • loeg 1 hour ago
              I don't agree that your example pattern matches to the example I'm complaining about. vfs_rename() is using old_dir's vtable on old_dir. The vtable matches the object.
              • ryao 1 hour ago
                It is not a vtable. It is a structure of function pointers called struct inode_operations. It is reused for all inodes in that filesystem. If you get it from one callback, you can safely use it on another struct inode on the same filesystem without a problem because of that, because nobody uses this like a vtable to implement an inheritance hierarchy. There are even functions in struct inode_operations that don’t even require the inode structure to be passed, such as ->readlink, which is most unlike a vtable since static member functions are never in vtables:

                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.

    • Galanwe 6 hours ago
      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.

          mystruct_dosmth(s);
          mystruct_dosmthelse(s);
      
      vs

          s->dosmth();
          s->dosmthelse();
      • 1718627440 6 hours ago
        My 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 5 hours ago
          This 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 5 hours ago
            True, if you don't trust the compiler to optimize that, then you must live with the C naming.
    • spacechild1 11 hours 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 10 hours ago
        In 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.
    • ActorNightly 8 hours ago
      You can also get clever with macros.
    • Gibbon1 6 hours ago
      The 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.

    • elteto 11 hours ago
      ...and C++ added explicit this parameters (deducing this) in C++23.
    • ryao 8 hours 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 8 hours ago
        It’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 8 hours ago
          If 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 7 hours ago
            Not 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 7 hours ago
          Good 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.
  • ryao 1 day 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.

    • pavlov 11 hours ago
      In Smalltalk and Objective-C, you just check at runtime whether an object instance responds to a message. This is the original OOP way.

      It's sad that OOP was corrupted by the excessively class-centric C++ and Java design patterns.

      • moregrist 9 hours ago
        > 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.

        • chunkyguy 7 hours ago
          > This introduces performance issues larger than the typical ones associated with vtable lookups.

          Don't know about other programming languages but with Objective-C due to IMP caching the performance is close to C++ vtable

            Name Iterations Total time (sec) Time per (ns)
            C++ virtual method call 1000000000 1.5 1.5
            IMP-cached message send 1000000000 1.6 1.6
          
          
          https://mikeash.com/pyblog/friday-qa-2016-04-15-performance-...
        • 1718627440 7 hours ago
          When you fear about the branch miss, then you can also just call the method and catch the SIGSEGV. I think if you allow for the possibility of there being no implementation, then you can't really not have some decision there. This would also apply for say a C++ virtual method.
      • pjmlp 11 hours ago
        Actually I would say that it is sad that developers learn a specific way how a technology is done in language XYZ and then use it as template everywhere else, what happened to the curiosity of learning?
      • bluGill 9 hours ago
        Java is excessively class centric. C++ often is, but it need not be and developers are movingiaway.

        smalltalk is not original OO - c++ took oo from simula which was always a different system.

      • mettamage 11 hours ago
        Wait, so in obj-c, could you also write some kijdnof doesnotunderstand method to achieve some dynamic method dispatch?
        • pjmlp 11 hours ago
          Yes, that is how microservices were implemented in the days of NeXTSTEP. with PDO.

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

          • astrange 34 minutes ago
            It's how many things are implemented, like UI message routing, IPC, undo, test mocks.
          • mettamage 7 hours ago
            So why did Swift become a thing? Or does Swift has this too?
            • astrange 35 minutes ago
              Swift isn't messaging-based, it's protocol-based. (Except it's also messaging-based if you use @objc.)
            • saagarjha 44 minutes ago
              Because Swift adds many other features.
            • bitwize 3 hours ago
              Because Objective-C does not have foo.bar() style method calls and that's what everybody else uses and wants.
      • MangoToupe 1 hour ago
        Sure, but that is a a horrible coding pattern. Even good smalltalk code doesn't rely on this. It's dogshit and lazy
    • 1718627440 11 hours ago
      > This technique predates object oriented programming.

      I would rather say that OOP is a formalization of predating patterns and paradigma.

      • ryao 7 hours ago
        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.

        • p_l 5 hours ago
          OOP does not disallow instantiation with unimplemented functions, it's just an artefact of implementation in some languages.
          • kazinator 1 hour ago
            OOP only disallows inheritance with unimplemented functions when it's a contract violation.

            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.

            • ryao 1 hour ago
              I would call returning something being implemented as a stub rather than being unimplemented. When something is unimplemented and you try to call it, you crash due to a NULL/invalid pointer dereference, not get an error back. Of course, as far as getting things done is concerned, the two might as well be the same, but as far as how the language works, the two are different.
              • p_l 57 minutes ago
                Crashing is optional, depending on error model of the language. C has pitiful error model, thus you'll usually end up jumping to 0... but I recall at least one C environment where that gave you an error back instead of crash.

                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.

        • 1718627440 6 hours ago
          > like instantiation with unimplemented functions

          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.

          • ryao 1 hour ago
            Unlike OOLs, C does not enforce contracts around structures (objects). You are free to do literally anything you want to them.
    • trws 1 day ago
      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.
      • ryao 11 hours ago
        My point is that this pattern is not object oriented programming. As for a default behavior with it, you usually would do that by either always adding the default pointer when creating the structure or calling the default whenever the pointer is NULL.

        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.

        • f1shy 10 hours ago
          To be fair, OOP is not 100% absolutely perfectly defined. Strustrup swears C++ is OOP, Alan Key, at least at some point laughed at C++, and people using CLOS have yet another definition
        • 1718627440 7 hours ago
          > My point is that this pattern is not object oriented programming.

          Isn't this exactly how most (every?) OOP language implements it? You would say a C++ virtual method isn't OOP?

          • ryao 7 hours ago
            Member function pointers and member functions in C++ are two different things. Member function pointers are not OOP. They are data abstraction.

            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++).

            • 1718627440 6 hours ago
              Yeah, but the compiler implements these by adding vtables, propagating vtables values across inheritance hierarchies, adding another parameter.

              You claim when the compiler does this, it's OOP, but when I do it, it's not?

              • dragonwriter 5 hours ago
                Ìf you do it, it can still be OOP, its just not in an OO language. People have trouble separating using a paradigm and using a language focused on the paradigm, for some reason.
              • ryao 5 hours ago
                The entire point of OOP in every OOP language that I have ever used has been to have the language try to constrain what you can do by pushing restrictions on syntactic sugar involving objects, inheritance and encapsulation, so I would say yes. The marketing claims that people will be more productive at programming by using these.
                • 1718627440 5 hours ago
                  Yes, you need to have that to have an OOP language. OOP is object-oriented _Programming_, it's about how you program, not what features the language has.
                  • ryao 1 hour ago
                    In hindsight, I had your remark confused with another remark insisting that struct inode_operations is a vtable, despite it having what would be static member functions in C++, which are never in vtables, and there being no inheritance hierarchy. If you are disciplined enough to do what you said, then I could see that as being OOP, but the context here is of something that is not OOP and only happens to overlap with it. The article mentions file_operations, but ignores that it has what would be a static member function in C++ in the form of ->check_flags(), which is never in a vtable.
        • naasking 10 hours ago
          > My point is that this pattern is not object oriented programming.

          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.

          • ryao 6 hours ago
            Object oriented programming implies certain contracts that the compiler enforces that are not enforced with data abstraction. Given that object oriented programming and data abstraction two live side by side in C++, we can spot the differences between member functions that have contracts enforced, and members function pointers that do not. Member functions have an implicit this pointer, and in a derived class, can call the base class version via a shorthand notation to the compiler (BaseClass::func() or super()), unless that base class version is a pure virtual function. Member function pointers have no implicit this pointer unless one is explicitly passed. They have no ability to access a base class variant via some shorthand notation to the compiler because the compiler has no contract saying that OOP is being done and there is a base class version of this function. Finally, classes with unimplemented member functions may not be instantiated as objects, while classes with unimplemented member functions pointers may.

            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.

            • 1718627440 6 hours ago
              So you can opt out or in to syntactic sugar, that makes C++ an interesting and useful language, but how you implement OOP, doesn't really affect if it is OOP.
              • ryao 5 hours ago
                By this logic, C is an objective oriented language. It is widely held to not be. That is why there were two separate approaches to extend it to make it object oriented, C++ and Objective-C.
                • 1718627440 5 hours ago
                  You can implement OOP in C as you can in any language, the article is an example of this. C is not an OOP language in any way, it doesn't have any syntactic features for it and use the term "object" for something different.
                  • ryao 58 minutes ago
                    The article mentions file_operations, but ignores that it has what would be a static member function in C++ in the form of ->check_flags(), which is never in a vtable. The article author is describing overlap between object oriented programming and something else, called data abstraction, which is what is really being done inside Linux, and calling it OOP.

                    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.

            • teo_zero 3 hours ago
              > Object oriented programming implies certain contracts that the compiler enforces

              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.

              • ryao 27 minutes ago
                > Sorry, but where did you got this definition from?

                It is from experience. 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.

    • kerblang 7 hours ago
      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)

    • yndoendo 9 hours ago
      Inheritance is not needed when a composite pattern can be used.

      class DefaultTask { }

      class SpecialTask { }

      class UsedItem {

          UsedItem() { _task = new SpecialTask() }
          
          void DoIt() { _task.DoIt() }
      }

      Is python a OOP language? Self / this / object pointer has to be passed similar to using C style object-oriented / data abstraction.

      • 1718627440 7 hours ago
        The interesting thing is, that in the OOP implementation inheritance IS composition of vtables and data. It's really only syntactic sugar, that is sometimes not unambiguous.
        • zozbot234 7 hours ago
          This is not quite correct. OOP implementation inheritance involves a kind of "open recursion" (that is, calls to base-class methods can end up dispatching to implementations from a derived class) that is not replicated with pure composition. All method calls, including method calls that originate from code in some class anywhere in the hierarchy, ultimately dispatch through the vtable of whatever object they're called on.
          • 1718627440 6 hours ago
            But that's exactly the same you would need to implement manually when you use composition. When constructing, you also need to construct the contained objects, when doing something that should affect a contained object, you need to dispatch it.

            When a method is never overridden, it doesn't need to be in the vtable.

            • zozbot234 6 hours ago
              That's not what people usually mean by "composition" though. The whole point of "use composition over inheritance" is to avoid that behavior.
              • 1718627440 6 hours ago
                I don't see how you can use composition without eventually calling methods of the contained objects. Every method of the outer object either uses an inner object or it doesn't. Yes, using the inner object doesn't always means just delegating to a single call. You maybe would implement the outer by calling an inner objects method multiple times or different methods, but nothing stops you of doing the same thing with a super class. When you don't call to an inner object, it's the same as you adding another method to the subclass, without it being present in the parent class.

                I think composition over inheritance is only about being explicit. That's it.

      • maleldil 7 hours ago
        Python doesn't require self to be passed. You need it in method definitions, but not calls.
        • 1718627440 7 hours ago
          But you can do it. Actually you can call instance methods of other classes and change the class of an instance in Python like in C, but this dynamism is probably what makes it slow. Also doing that will make the program quite complicated, more so than in C, since Python also abstracts about this.
    • mistrial9 12 hours ago
      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.

      • ryao 11 hours ago
        Compiler 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 11 hours ago
          Actually, no.

          "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.

  • SLWW 3 hours ago
    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.
  • tdrnl 11 hours ago
    A 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/1536b7e206e51488... [1] https://blog.drnll.com/tmux-obj-oriented-commands

  • accelbred 8 hours ago
    I usually put an inline wrapper around vtable functions so that `thing->vtable->foo(thing, ...)` becomes `foo(thing, ...)`.
  • BinaryIgor 9 hours ago
    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.
    • 1718627440 7 hours ago
      Whenever 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. 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.
    • davikr 8 hours ago
      Probably into the High C Compiler.
  • munchler 10 hours ago
    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.
    • 1718627440 10 hours ago
      What do you think inheritance is, if not composition of vtables? What do you think classes are, if not a composition of a vtable and scoped variables?
      • munchler 10 hours ago
        Those "scoped variables" are the difference. Mutable state adds a great deal of complexity.
        • 1718627440 10 hours ago
          And the style presented in the article uses vtables with "scoped variables". How do you conclude it's "not full object-orientation"?
    • PhilipRoman 10 hours ago
      Field inheritance is surprisingly natural in C, where a struct can be cast to it's first member.
      • 1718627440 10 hours ago
        Note 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);
        • teo_zero 2 hours ago
          In 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.
        • PhilipRoman 9 hours ago
          Yeah you're right, I meant the other way around. Also another loosely related idea is the container_of macro in Linux kernel.
          • 1718627440 9 hours ago
            Yeah, my idea is literally native type-safe support of container_of for assignment in the compiler.
  • nphardon 8 hours ago
    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.
  • MangoToupe 1 hour ago
    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.
  • 2OEH8eoCRo0 9 hours ago
    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?
    • nphardon 9 hours ago
      Here'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 4 minutes ago
        What 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 7 hours ago
      C makes it obvious were you use that dynamism and where you don't. Syntactic sugar doesn't really make that much of a difference and also restricts more creative uses.

      The C syntax is not really that complicated. Dynamic dispatch and virtual methods was already in the article. Here is inheritance:

          struct Subclass {
              struct Baseclass base;
          };
      
      That's not really that complicated. Sure, you need to encapsulate every method of the parent class, if you want to expose it. But you are also recommended to do that in other languages, and if you subclass you probably want to slightly modify behaviour anyway.

      As for stuff like templates: C doesn't thinks everything needs to be in the compiler. For example shadowing and hiding symbols can be done by the linker, since this is the component that handles symbol resolution across different units anyway. When you want templates, either you actually want a cheap way of runtime dynamism, then do that, or you want source code generation. Why does the compiler need to do that? For the basics there is a separate tool in the language: the Preprocessor, if you want more, you are free to choose your tool. If you want a macro language, there is e.g. M4. If you want another generator just use it. If you feel no tool really cuts it, why don't you write your code generator in C?