C++ Weekly - Ep 263 - Virtual Inheritance: Probably Not What You Think It Is
Вставка
- Опубліковано 7 вер 2024
- ☟☟ Awesome T-Shirts! Sponsors! Books! ☟☟
Upcoming Workshop: C++ Best Practices, NDC TechTown, Sept 9-10, 2024
► ndctechtown.co...
Upcoming Workshop: Applied constexpr: The Power of Compile-Time Resources, C++ Under The Sea, October 10, 2024
► cppunderthesea...
T-SHIRTS AVAILABLE!
► The best C++ T-Shirts anywhere! my-store-d16a2...
WANT MORE JASON?
► My Training Classes: emptycrate.com/...
► Follow me on twitter: / lefticus
SUPPORT THE CHANNEL
► Patreon: / lefticus
► Github Sponsors: github.com/spo...
► Paypal Donation: www.paypal.com...
GET INVOLVED
► Video Idea List: github.com/lef...
JASON'S BOOKS
► C++23 Best Practices
Leanpub Ebook: leanpub.com/cp...
► C++ Best Practices
Amazon Paperback: amzn.to/3wpAU3Z
Leanpub Ebook: leanpub.com/cp...
JASON'S PUZZLE BOOKS
► Object Lifetime Puzzlers Book 1
Amazon Paperback: amzn.to/3g6Ervj
Leanpub Ebook: leanpub.com/ob...
► Object Lifetime Puzzlers Book 2
Amazon Paperback: amzn.to/3whdUDU
Leanpub Ebook: leanpub.com/ob...
► Object Lifetime Puzzlers Book 3
Leanpub Ebook: leanpub.com/ob...
► Copy and Reference Puzzlers Book 1
Amazon Paperback: amzn.to/3g7ZVb9
Leanpub Ebook: leanpub.com/co...
► Copy and Reference Puzzlers Book 2
Amazon Paperback: amzn.to/3X1LOIx
Leanpub Ebook: leanpub.com/co...
► Copy and Reference Puzzlers Book 3
Leanpub Ebook: leanpub.com/co...
► OpCode Puzzlers Book 1
Amazon Paperback: amzn.to/3KCNJg6
Leanpub Ebook: leanpub.com/op...
RECOMMENDED BOOKS
► Bjarne Stroustrup's A Tour of C++ (now with C++20/23!): amzn.to/3X4Wypr
AWESOME PROJECTS
► The C++ Starter Project - Gets you started with Best Practices Quickly - github.com/cpp...
► C++ Best Practices Forkable Coding Standards - github.com/cpp...
O'Reilly VIDEOS
► Inheritance and Polymorphism in C++ - www.oreilly.co...
► Learning C++ Best Practices - www.oreilly.co...
What he didn't touch on that has bitten me before is that virtual base class is default constructed before anything else, so the constructors of the derived have no control over them.
I actually only recently learned about this myself, I do try to avoid designs like this, so the practical issues rarely come up for me.
Yes this one is very interesting... Item 9 from Effective c++
You are wrong. It is NOT default constructed (or put it differently, this isn't the requirements). It is constructed BEFORE its derived classes (but that is normal behavior).
@@arturczajkowski4255 and what parameters are given to that constructor? The default ones.
@@reverendragnarok No, not default ones. You can call any constructor with any params.
tried that once. the more i coded it, the darker it got outside, the fuller the moon got, and the forest somehow grew closer while wolves started howling begging me to join them.
Good way to describe it. It's a warning sign.
The thing I love about C++ is its “attitude” of “there needs to at least be a way to do it”. Nothing should be actually impossible.
Haha yes and 3 years later everything is in the standard
Except having a stable ABI. Having a stable ABI is actually impossible.
Or hygienic macros. Or reflection (for now at least). Or built-in garbage collection. Or readable compiler errors. Or a consistent std.
@@ruroruro Tbh, gcc and clang have had provided pretty stable ABIs for a while now and even have options to avoid the std::string ABI break. The committee has also routinely rejected features that would cause compiler vendors to have to break ABI. So while C++ doesn't technically have a stable ABI, if you're on Unix using clang or gcc, your compiler vendor (and STL vendor) are providing a stable ABI based on Itanium.
@@lincolnsand5127 well, I just recently had a bunch of problems with basic string ABI due to nvcc not supporting newer GCC versions.
Also, ABI stability includes the binary representation of user-generated content, not only the std. So the lack of encapsulation for private members breaks the ABIs of user programs/libraries.
> _"needs to .. be a way to do it."_
really? for the following situation too? I was facing problem at step 4 & 5:
*Summary:*
+ initialising parent class's static member variable(s) in child class, and
+ using those initialised values to call a parent class static member function
*Details:*
1. I want to create static member variable (of type struct: sting, char, int or either individual variables), say "classSpecs" inside a "Base" class
2. Now I want to create a static member function "putName()" to cout this "categorySpecs"
3. Then i want to create hierarchial inheritance, i.e. two subclasses say "Child1", "Child2"
4. Then I want some way to initiate the value of that struct "categorySpec" inside those subclasses.
5. Then call "Base::putName()" from inside some (static?) member function of those subclasses by passing that initiated value.
One important detail that you miss is that the virtual base must only be constructed once. And so then you have an interesting situation if both Intermediate2 and Intermediate3 call the constructor for Base. I believe that those constructor calls will be ignored, and Derived will have to call the constructor for the virtual Base. So if Base doesn't have a default constructor, Derived will have to call a constructor for a class that it isn't obviously derived from.
I haven't tested this. This is just from what I recall of how this feature works. So, in other words, I might be wrong.
It may be good to use C++ insights on that code
That's correct. I had this problem some weeks ago.
@@ub880 - Yeah, I had a comment here where I posted a little test I did with compiler explorer that showed that my memory was correct. Unfortunately, because it contained a link to compiler explorer, UA-cam ate it.
Intermediate2 & Internediate3 will ignore the call to construct Base, that is if they are part of Derived.
But here is the rub:
Suppose you have some standalone objects Intermediate2 & Internediate3. Then they will call the Base constructor.
How is this done? There is usually a hidden bool parameter which if true constructs Base and if false does not.
So for Derived, that is true, Base is constructed, but then when Intermediate2 & Internediate3 are constructed, this passed as false.
For standalone objects Intermediate2 & Internediate3, this hidden bool parameter constructs Base.
Now I think about, there must be something similar for destructors, because the same issue arises.
One super useful feature that virtual inheritance gives you is mixins. You can define lots of pure virtual methods in 'Base' and then implement different subsets of them in different 'Intermediate' classes. I think of it as 'Base' defining different features for the client, say feature A and feature B. Now, for each feature you *could* have different implementations in 'Intermediate' classes, but for each feature you pick one. So, feature B could be implemented in different ways, but for one particular 'Derived' class you pick just one of the many 'Intermediate' classes.
When C++ does multiple inheritance, everybody goes “ewww”. When other languages do it, they call it “interfaces”, “mixin”, etc and it becomes the most important feature of that language.
Me - programming in C++ for nearly a decade and never even heard about virtual inheritance till this video
Thanks Jason, now I got some imposter syndrome haha - super helpful video though
Diamond inheritance is uncommon design and usually a symptom of poor design.
@@BigPapaMitchell thanks, so I’ll just assume that means I’m good at designing inheritance haha
@@BigPapaMitchell Not a very experienced C++ programmer here. But I think Diamond inheritance happens often if all/many of your classes inherit from a base class that offers some functionality, for example, something similar to Java's 'Object'. In such a case, everything will be alright until there's a need to do multiple inheritance at some point.
It's just one of those poorly asked interview questions.
Instead of asking how to design against the problem, they ask how to workaround the problem with language syntax.
@@BigPapaMitchellYes and no. I would say that Qt is well-designed. And in Qt, everything inherits from QObject.
So you're in a case of diamond inheritance as soon as you do multiple inheritance. I guess that multiple inheritance itself is an antipattern, that's also why many languages don't support it.
I’m sorry if I’m repeating someone’s comments, but this kind of trick is quite useful. C++ tuples heavily depends on multiple inheritance and we can do in C++ all sort of limited version other languages have, such as mixin, interfaces (even “modern” ones with “default” methods).
how mixin inheritance? also, how kotlin's extension functions?
Avoiding multiple inheritance altogether improves code "reviewability" and maintainability. It is interesting how hard it is some times to keep things simple when you have many features. It's like an impulse of thinking "If I have this feature I must use it".
@@mattmurphy7030 Generally speaking, older "more experienced" programmers tend to stick with what they know best and avoid implementing newer paradigms while younger more up to date graduates tend to miss the point entirely as to what kind of software design helps the most a Company to accomplish high maintainability and "refactorability". Microservices design kinda came into being to address exactly this problem in the industry.
@Matt Murphy Brilliant. ^_^
Not at all. Multiple inheritance is modelling situation. If you have a Mixin Class, your class might provide Services 1 and Services 2, both orthogonal to each other.
Multiple inheritance allows you to model Mixin's. I certainly would not use multiple inheritance just for the sake of it.
For that matter, iostreams in the standard - that uses multiple inheritance.
@@stephenhowe4107 Avoiding means alternatives to MI are often preferable for various reason beyond your personal know-how. If you're an expert and you know from heart all the corner-cases of implementing multiple inheritance and you do it well and you alone are going to maintain it, then of course. It will always be an option regardless of what people generally agree are good practices.
Very interesting and well explained: C++ has a world inside it.
Thanks for this one Jason !!! Always concise and to the point. It's such a blessing catching your *c++* tutorials always ;-)))))))))
Amazing channel.
This is great, I just discovered this a few days ago and was researching it myself, this did a really good job of explaining it.
Brilliant explanation! Thanks.
Excellent explanation !
This video could have spared my coworker from explaining virtual inheritance many times to me.
Diamond inheritance solve ! (never seen use case so far)
It's fantastic for implementing the middle out algorithm.
It's not quite true that it has nothing to do with virtual functions. Like virtual functions, this is adding an additional pointer to the class, here to keep track of the base. Was hoping you would go into that implementation more.
Watch Arthur O'Dwyer's CppCon talk dynamic_cast from scratch. He goes into the details of virtual inheritance.
The actual implementation is up to compiler.
It doesn't add a new pointer, just add more entries in the vtable, no ? Not 100% sure.
@@LEpigeon888 For virtual functions, it just adds more entries into the vtable, of course in the form of function pointers. I recommend watching the talk i mentioned above.
@@6754bettkitty I've read quuxplusone.github.io/blog/2019/09/30/what-is-the-vtt/ , he said « Suppose Cat has a virtual base Animal. Then vtable for Cat holds not just function pointers to Cat’s virtual member functions, but also the offset of Cat’s virtual Animal subobject. ».
So as far as i understand, everything is in the vtable, there is no additional pointer / data inside the class. At least for the Itanium ABI.
oh man, you could do another whole video on pointers to member functions and fields in virtual inheritance hierarchies, their varying size, and how dispatch is implemented differently by different compilers.
I have no problem with virtual inheritance and even multiple inheritance per se, it's usually very clear and a good way for abstracting the actual world. But if one run into the diamond inheritance situation, it's a sign for rethinking the structure of what you want to abstract and how to do it the most clear way. I have stumbled into it more than once and realized that I did things overly complex. If you start writing those extra virtuals on the inheritance, just be aware of what you are actually trying to achieve. You may than have a structural design problem.
Virtual inheritance works somehow differently. In the example given with int1 and int2, then derived inherits from base directly. This is possible to check as if you make base constructor private and make int1/int2 friends of base, with virtual inheritance it will not compile as derived cannot have access to base private constructor.
The only use I can think of for virtual inheritance is to make a class final, you use a template class with a private constructor that will friend the template class parameter and then inherit virtual from the template and pass the same class as template parameter (before c++11)
Mindblowing
I knew this for a long time, but I think it only make sense if you show how inheritance can be implemented in memory (with some graphics). Otherwise it looks totally random. Once you see how things can be put together in memory when working with inheritance, the feature becomes "logical" (though super niche)
Actually this is not written in the standard for how the compiler implements the inheritance itself. It could be very well imagined yes, but there is nothing to rely on here. But you're right, it's at the same time interesting to get an idea of how virtual inheritance actually is laid out in memory. Not least for the indirections of pointers in assembly that it creates.This affects cache and in it's own way. The same that unrolled loops would do for cache hits and traversing lists etc., cache is an evergoing interesting problem if you think of optimisation.
@@larswadefalk6423 Yes I know it's not specified, but explaining it implies having to explain that it's somewhere in memory, and that somewhere have an impact.
I thought you were going to cover virtual operators.
I have only had 1 situation where a virtual < operator was required
If you have a base class that represents the States of America and you wish to have a std::set of them but the order could be Population, Area Size, Wealth etc, you might have a pure virtual < operator, and Derived classes where the < operator is overridden and provides ordering by Population, Area Size etc. Now you can do that. And call base class services.
The magic glueing together of virtual inheritance in to all pointing to the same thing seems a bit weird. I don't think I have ever used or wanted to use this feature but now that I know it exists it will surely nag on my brain.
One useful case I know of: imagine you have your own intrusive_ptr, and your classes are refcounted. And let's say you have an intrusive_ptr and an intrusive_ptr that both refer to the same object "Foo : public Producer, public Consumer".
If you want to maintain the property that it can be owned as either of them, in languages like Java or C# it would just work (both `Producer foo` or `Consumer bar` can refer to and own the same implementation of both interfaces). In C++, it can be somewhat tricky.
But with virtual inheritance, you can make this work, by making your interface-like abstract classes virtually inherit from some RefCountBase: "Producer : public virtual RefCountBase", "Consumer : public virtual RefCountBase". Now "Foo : public Producer, public Consumer" has only one internal copy of RefCountBase, so both intrusive pointers will be working with the same atomic refcount.
You mentioned that virtual inheritance has nothing to do with virtual function calls. But it does have *something* to do with virtual function calls: When you use virtual inheritance, every call to a base class member function or access to a base class member variable is indirect and needs to use the vtable to get an offset. In this sense the performance costs are very similar to calling a virtual function.
In some ways not quite as bad because there's no data dependency between the vtable load and the target of the `jmp` or `call` instruction which helps the compiler and CPU optimize the function call (it can even be inlined unlike a virtual call). In some ways worse because you now need the vtable to access member variables, not just to call functions. But long story short the use of the keyword "virtual" isn't an accident and actually a pretty good name given the similarities.
Did someone say convoluted ?
I was waiting for the epiphany here but it didnt come. It works exactly how I expect.
The odd thing to me about virtual inheritance is that the intermediate class must make the choice of how the final composition will look. With virtual inheritance the indirection overhead of accessing the base may always be incurred even if the final hierarchy ends up including only one instance of the base class.
The requirement to construct the virtual base in the most derived class is also an oddity, albeit an understandable one. The most unpleasant part of that for me is that sane looking code with an explicit construction in the intermediate class is not used, but instead it might be an implicit default construction in the most derived class and as of my last check that intermediate constructor call might be dead code if the class has a pure virtual call or no public constructors, etc. That to me is something that isnt particularly intuitive for the uninitiated.
I wait for these episodes almost like I wait for my favorite tv series. haha
A great example of why other languages disallow multiple object inheritance. :)
Well, I guess I'm only one in the comments, who learned this concept in the early days of my C++ journey :)
(It's been 2yrs roughly, since I learned it)
So, should we just avoid inheritance and simply favor composition? Or is it virtual inheritance in every case? I was under the impression that inheritance was about marshaling, structuring, and organization of type hierarchies? Rather than the marshaling, structuring, and organization of values? In the all virtual case, it is about types. In the non-virtual or partially-virtual case, it seems to be about values?
It is what I thought it was because I watched Arthur O'Dwyer's fantastic talk "dynamic_cast" from scratch a few months ago lol. Anyways, good video on the topic
As I'm watching this the only thing coming to mind is "inheritance is the base class of evil" lol.
Not at all. It is useful for modelling a real life situation
That's why Rust doesn't have it.
@@mattmurphy7030 Sorry, man. I disagree with you. Every time I saw inheritance and casting used in a company, it lead to disaster. Heavy inheritance usage assumes that models are rigid and will never change, and assume that things that look alike will always be alike, and people are "good" at identifying patterns, which makes them assume that things are the same, and assume that if it walks like a duck, then it's a duck, until you face a flying goat in your program and your whole design model is screwed. All the time, we hear "use composition instead of inheritance", and there's a reason for it. The only time where inheritance is useful is when you want to use it as an interface, and Rust offers that. To me, that issue, 349, sounds like developers used to making bad designs, and are complaining that they can't do these bad designs anymore. Like... how can you see someone asking for a "downcast" and still not think it's a bad idea? Apparently people crave complaining about dynamic_cast, and rust isn't giving them that chance.
@@mattmurphy7030 it's "controversial" only because people got used to it, for the last 50 years, not because it's a great idea. It was a good idea back then when programs were very simplistic and computer modelling was a new thing, not in this decade.
And if UE uses inheritance successfully, it doesn't mean at any capacity that this is the only way, nor have I said that inheritance cannot be useful, but what I said is that inheritance can be useful for the short term, until things break, and then you patch the patch 559 times until your code becomes legacy code that falls under its own weight or increase the cost significantly. Again, keeps happening, even if there are exceptions, but that's the rule it seems.
And notice something, it's not that inheritance is inherently bad. The problem is that programs are almost never static. There's a reason why waterfall is dying and everyone is chasing scrum and agile. The problem with inheritance is that it assumes you know everything at the time of designing the code, which is not even close to true and leads to high code coupling based on coincidences, and ironically, was kind of OK 50 years ago with waterfall. It's the wrong thing to assume nowadays. That's the issue with inheritance.
@@SamTheSciencerAtheist Some people cannot imagine a better way of doing something. Like at all. It simply never dawns on them.
Been waiting for this topic. I've had to overcome the diamond problem a few times. Does virtual inheritance cause a vtable? Or is it all sorted out at compile time
Depending on the implementation, according to others in the comments, and from my own understanding: the instance ends up having to track an extra pointer to the true base class, which should be first in the instance memory. Next would be the vtable for that shared base, then all it's memory. Then it would be the vtable for one of the inherited classes and all it's components and then the next class before the fully derived class. So in the most roundabout way of answering; yes, it should cause a vtable. What impact it has on the application should be benchmarked, but if the standard isn't afraid to use it in iostream then it must not be completely evil 😂
@@sorakatadzuma8044 thank you very much :)
in my opinion, the weirdest (most annoying) part of this situation is that we defy the open close principle.
we have intermediate1 and intermediate2 and they work fine, but to implement derived :intermediate1, intermediate2 we need to go and change them.
which means that any other derived classes that use them are now under suspicions.
question:
what would happen if I marked all my inherited classes as virtual inheritance?
I wondered this too. It almost seems logical to make all inheritance virtual unless you have a specific need not to.
It also begs the question about thread safety and having to make everything in base atomic or whatever since you have multiple routes to get at it
> _"but to implement derived :intermediate1, intermediate2 we need to go and change them."_
> _"what would happen if I marked all my inherited classes as virtual inheritance?"_
yeah, i had exactly same grudge, and consequently same question. tbh, i really think i will not be passionate about cpp...
I loved learning about multiple inheritance years ago and THEN being told, "It's possible, but if you find yourself needing it, maybe take a step back to the diagram-design level before you proceed." So, I've just avoided it. Diamond structures bother me. Trees shouldn't do that. I'd love to see an example of where it is actually a clever solution.
I have absolutely no problem with multiple inheritance, but diamond structures are generally terrible. I'm pretty sure I actually say that in the video, no?
I've used it sometimes with interface base classes. Something like:
// Interface (abstract classes):
class IBase { };
class IDerived : public virtual IBase { };
// Concrete implementations:
class Base : public virtual IBase { };
class Derived : public Base, public IDerived { };
The only thing that bother me is where the virtual keyword is put. I guess it's for technical reasons but it doesn't make sense from logical point of of view. In my case above, the only class which is "aware" of the multiple inheritance is Derived. So to me it would make more sense to put the virtual keyword in its inheritance:
class Derived : public virtual Base, public virtual IDerived { };
whether it uses a normal or virtual inheritance has an impact on the vtable and class layout, and since you want the same type to be the same everywhere, it requires every intermediate "leaf" of your inheritance tree to be marked as needed
Next video is about order of initialization and virtual destructors in overly complex diamond inheritance constructs?
If anything next episode is just a single title "avoid virtual inheritance and diamond hierarchies."
30 seconds of the title screen :D
But what happens if thia ia combined with virtual funnction let's base has a virtual function f that both intermidiate1 and intermindiate2 derive from (both inheriting virtually), what function will you call then just the base version or is it still considerd abigoua?
Did you test it to find out?
i wonder if people use these kind of things.. virtual multiple inheritance
I have seen it used. At the time it was the only solution we could find (needed for subclassing from QObject + another type)
I was wonder, can we do patterns other than a diamond? like two separate virtual bases? hope you know what I mean :)
In C++? Surely you can do it. You can have a full mesh of base and intermediates, if your sanity (and coworkers) allows it. :)
@@ecosta But how? can you show me example code?
suppose we have virtual Base, inherited to A1 and A2 and another virtual Base of same type, inherited to B1 and B2
or I need to use two "empty classes" so two virtual Base to be of different types?
@@nmmm2000 I can't post links here and I'm not sure if I can describing it with just text, but I did some experiments in Compiler Explorer, and I was able to navigate using a syntax like "derived.Int1::Base1::value". Worst case I had to introduce a temp ref, like "Int1 & i = derived; i.Base1::value".
What is the tool that allows you to draw on the screen called?
It's GInk
Dimond problem!
Click-baity title. Also, I would have liked you to show how virtual base classes are implemented by the compiler.
Not really click-baity, at least 75% of the people who responded Twitter discussion about virtual inheritance thought that I was referring to virtual functions.
@@cppweekly I still think it would be cool to show how virtual base classes are implemented in a future video though.
For me, the most obvious question becomes, why is this valid C++ code and shouldn't these things be removed from the language entirely. C++ after all, is a language that strives for efficient and correct code.
C++ doesn't distinguish between classes and interfaces, so multiple inheritance is a more generic solution that has other uses. But with multiple inheritance of classes with data members, you need to decide if you want to share the base class or not. So C++ provides both ways of doing it, with the more expensive thing being the more verbose and opt-in.
I think it's a sign of a structure that may be going in the wrong direction. Yet still it can be useful in rare cases and I don't like to blaim a language for being generic and flexible.
That's why we have people to learn us from their mistakes on how to do things the right way.
Think of this example, java forbids operator overloading as it was considered by the creator to be a "bad thing". But in actual cases things means different depending on the context.
Even math itself has operator overloading implicitly, compare for instance matrices between simple scalar values.
Therefore it makes absolute sense to have a notion of operator overloads depending on the type, as long as the semantics make sense.