Example: if one has tree nodes with various slots that represent children and you want to write a tree traversal function, you put each slot in a superclass, inherit from those superclasses in the correct order, and then write a method for each superclass that calls the child at that slot. The methods are combined in the right order automatically in a PROGN method combination.
If B and C both have methods foo(), which gets called when you do d.foo()?
Seems like a real footgun requiring extra effort to avoid.
Really, all the problems with multiple inheritance are that the humans can't handle the complexity that results. The compilers can be made to do "something" that is arguably sensible.
I mean, it's just not that bad. I believe the commercial Lisp IDEs will just show you relevant info much like, say, Java IDEs, but even with a free Lisp you can still ask for it so you don't actually need to wonder what will happen as you're looking at a line. You just ask. The worst part of Lisp vs. C++ on multiple inheritance, I think, where it can be more confusing for Lisp is that Lisp will just overwrite slots (fields) sharing the same name, whereas C++ will shadow them. On the other hand methods aren't owned by individual classes in Lisp, so you get multiple dispatch by default. Lastly the presence of :before / :after / :around methods, combined with multiple dispatch, make it pretty straightforward to achieve behaviors through mixins that require pretty complex contortions otherwise in C++. (Or Java.) The behavior of those "auxiliary methods" is straightforward to reason about. All :before methods run before the most specific primary method, in most-specific-first order, and all :after methods run after the least specific primary method, in least-specific-first order.
I'm probably going to convince some people otherwise by giving some more specifics, but as a minor example, consider a silly "game object" style class. I can always ask any class (e.g. an asteroid), hey, what's your class precedence list? (closer-mop:class-precedence-list (find-class 'asteroid)) returns a list of class objects: asteroid, game-object, sprite, add-groups-mixin, cleaned-on-kill-mixin, standard-object, slot-object, and T. From the source code where the defclass is, only game-object is shown. If you look at game-object, only sprite and the two mixins are shown as an example of multiple inheritance.
I don't need to call that function to get the info either, it's readily available by calling 'describe on the class. (I think even free editors like Lem or emacs can be configured to automatically show the description of things if you hover over them, I just type ,s in vim.) The description includes the same class precedence list info, tells me the direct superclasses, any subclasses, direct slots (fields directly defined on the class), inherited slots...
If I'm wondering what could happen if I call #'kill on an asteroid before I actually call it, I can ask with the built-in 'compute-applicable-methods function or 'closer-mop:compute-applicable-methods-using-classes, and it will show me the applicable methods are firstly the primary defined method, then an :after method due to the mixin.
I can also compute the actual effective method that will be called with 'closer-mop:compute-effective-method. For something like #'kill, it shows what happens first is the primary #'kill method, then the :after method. For something like #'draw, let's say I overwrote the base implementation, now it shows there's just one method call, with the potential for the next base class method if the specialized method happens to use 'call-next-method.
So in summary, the tools exist in various forms to wrestle the complexity and make it amenable to human understanding. Just like with tools such as cross-referencing, they help understand and create bigger systems, we don't have to limit ourselves to what can easily be done with physical code printouts and hand-made indexes.
More recently, the move seems to be away from class based object orientation (including inheritance) entirely.
On the other side of things, I've never heard people talk about Python's multiple inheritance with the same tone used for C++ - but then there are cultural differences in the language communities too.
Edit: I see that post refers to Dylan, which is more like CL than python in the important ways. IMO, sleeping on CL’s object system CLOS was a huge mistake of the “Java/C++ era” of our industry.
The problem with implicitly calling a parent constructor is that the child then can't control when or how (and with what arguments) it runs. So it's really pick your poison; either the child controls the call, at the risk of doing it wrong or not at all, or it doesn't but then certain things become impossible. (Or does CL have ways to e.g. modify the arguments that the :AFTER method sees, or run something else after that runs?)
CL lets you do both in various ways: the typical way to define a constructor is an :AFTER method that just sets the slots (fields in other languages) of the object and having a lot of behavior in constructors is unusual. You can also define an :AROUND method which would let a child class override the arguments passed to the constructor, with the downside that you can forget to CALL-NEXT-METHOD.
However, CL's approach to object-orientation is pretty radically different from Python's. It's not a "kingdom of nouns" system where classes contain behavior and state but rather classes can contain state and behaviors are on equal footing with classes in the form of generic functions. (If you aren't familiar, I found skimming chapters 16+17 here[1] very enlightening when I was first learning CL). Classes in CL frequently don't contain any state at all and merely exist to pick out an implementation of a generic function to be used. Generic functions establish a web of relations between classes because they dispatch on every argument and not just a "this" parameter.
C++ gonna C++, which means the language covers all the bases because some programmer might get mad if their use case wasn't accounted for.
C++ has something called virtual inheritance, wherein if subclasses B and C inherit virtually from A, any subclasses of both B and C will get one copy of A's state. Otherwise, they will get two copies: one from B and one from C.
This solves the problem of addressing concerns of all programmers w.r.t. the diamond inheritance problem, but makes the language more complex (and triggers my CPPPTSD).
Imagine one class inheriting from 50 other classes through multiple inheritance..
People really used to construct classes like:
"Iron Sword inherits from Iron which inherits from Metal which inherits from Meltable (which inherits from Temperature) and Material. But of course it also inherits from Sword which inherits from Weapon and Edged. Meanwhile Weapon inherits from Equipment which inherits from Ownable and Item which.." and so on.
Basically you make every aspect and attribute of an entity a class and then create your entity's class by mushing together all those classes through multiple inheritance. The results are..not pretty.
Such code quickly becomes very hard to comprehend and maintain.
That's an elegant example on coding Inform6 which transpiles against the Z Machine, but overall I won't use OOP outside gaming.
I've come across GUI and a database where I thought object orientation was nice, and I'm also fond of contemporary Smalltalk-like languages. I've made peace with Java, but if I have a choice I'll be in something Lisp-like or logic programming. Racket, Elixir, Scryer, that sort of thing.
90s C++, however, did.
Funny you should cite a game example. I once read about how the developers of StarCraft[0] ran into the same Goddamn inheritance problems I did when trying to build a custom game engine and a game with that engine. Adding behaviors via inheritance seemed like a good idea at the time (mid-late 90s), especially given all the propaganda we read from our C++ compiler manuals and such. But it turned into a situation where you either accepted multiple inheritance with all of its complexity and suck, including "which of the multiple base classes that implement 'foo' do I want when I call derived::foo()?" -- or resorting to delegates or other methods of composing behavior.
Me, for gaming, I became an ECS convert and haven't looked back. There are some pain points when writing a game in ECS style... but the advantages pay for the relatively minor pain many times over.
[0] https://www.codeofhonor.com/blog/tough-times-on-the-road-to-...
CFlingy is a particle spawner. Why does that have to be in the inheritance chain, instead of a trait you add to an object?
I did enjoy the read however! My own programming has evolved towards data oriented design over the years.
i've only found it summited a few times and only with comments here: https://news.ycombinator.com/item?id=10567360.
not a lot of comments and i suspect somewhat missing the point because this submission started with part 5, which intentionally is only part of the series exposing the pros/cons, limitations, etc. of various approaches.