I think you should add that *runtime* polymorphism was discussed in the video. C++ can also do compile time polymorphism (which doesn't have a runtime performance overhead).
"It is important to note that by default, functions with the same signature in a derived class are virtual in the parent by default." What? No? This isn't Java.
@@reromanSure, but it's such an odd mistake. It was such a detailed explanation, but it was just wrong. Especially for a whole video built around why polymorphism supposedly sucks.
@@michaelgreene6441Think about what you're suggesting. C++ is old. If there were such a drastic difference in behavior between C++ versions, that would actually be terrible. This is simply wrong.
@@Spirrwell It would literally mean you can't move a project further forward on C++ versions, which does happen occasionally, if you try to use auto in C++98 and and then move to C++11 it will throw an error where it was once valid, but such things are rare and not common use, auto was functionally worthless by 1995 and almost no one used the feature and it was deprecated for over a decade, and almost no other elements of the language were reworked like that, other case I can think of is the comma operator in square brackets, which again was a feature nobody used because the comma operator is almost never used for its return value. (and otherwise the implementation for a[1, 2, 3] wouldn't be how you could access a 3 dimensional container which they deemed a nightmare to do)
I have a number of issues regarding this video: First, the example given is a exceptionally poor choice. The number of unique cases is far to few for when Polymorphism should properly be used. With only "OP_ADD" and "OP_MUL" as outcomes, the code will compile into conditional jump statements. If there were more cases, the switch statement would likely be compiled into a jump table. This would have been a far better comparison to Polymorphism. As it stands, the video was effectively comparing if-statements to function pointers for a minuscule number of cases. Second, This explanation on how vtables are structured is based on the assumption that the vtable isn't embedded into the object. While I haven't encountered any compilers that do this, actual implementation of an object's vtable isn't standardized. This video can wrongly give off the assumption that it is standardized. Thirdly, in regards to why virtual methods are slow, while it is true that memory operations are slower than operations that take place on registers, the biggest slowdown actually comes from the fact your doing a dynamic jump. Modern CPUs try to fill the execution pipeline with as many instructions as possible with out-of-order execution by looking ahead for independent chains of instructions. This is far easier to do when the control flow of the instructions is static and works especially well when the branch predictor chooses the correct execution path. When a dynamic jump occurs, the processor basically has to halt everything until the memory location of where it's going is loaded into the processor. Forth, I must disapprove of the blanket "This is Bad" approach this video takes. Polymorphism is just like any other tool in programming: there situations where it is good and bad. When choosing what mechanism to use in programming, one needs to compare the benefits and cost. The video you've done shows Polymorphism being used in an inner-most loop. This is quite likely the worst case scenario for polymorphism. It simply isn't worth the overhead that comes with polymorphism. If you wanted to stick with the calculator theme for the video, better options like Square Root, Trigonometric Operations, or Logarithm would have been a fairer comparison. Lastly, the code presented at the start has some problems. Why are you using atoi in the conditional?! The code will be pointlessly executed every pass! Additionally, if you use optimization settings, the compiler might very well optimize away the entire body of the loop! If it tries to inline the operation code, it might very well see that the only one case is ever true and the operand assignments are pointless. This would result in an addition to a temporary variable that is only written to and never read from. Seeing it has no side effects, it might just empty the entire body of the loop. Unless you looked at the assembly, you wouldn't be able to tell how aggressively it optimized the code and if the tests were actually fair.
I also would like to add that the video invites to premature optimisation. 99.99% of developers will have bigger fish to fry in their codebase than two memory accesses
You left off Sixthly, he incorrectly states that you can't do OO programming in C. It requires more discipline, and you don't have the benefit of any sort of language features, but there is literally a book called "oo in c". Unless the book is several hundred blank pages, i haven't actually checked. Seventhly, sort of the the reverse of the above. If you really dying for that minuscule performance bump, you can write the code in C++ the same way you would in C. Without any late binding.
A non-virtual call is exceptionally fast, as it usually consists of a single instruction. On the other hand, a virtual call introduces an extra level of indirection, leading to the purported 20% increase in execution time. However, this increase is merely "pure overhead" that becomes apparent when comparing calls of two parameterless functions. As soon as parameters, especially those requiring copying, come into play, the difference between the two overheads diminishes. For instance, passing a string by value to both a virtual and a non-virtual function would make it challenging to discern this gap accurately. It's essential to note that the increased expense is primarily confined to the call instructions themselves, not the functions they invoke. The overhead incurred by function calls constitutes a minor proportion of the overall execution time of the function. As the size of the function grows, the percentage representing the call overhead becomes even smaller. Suppose a virtual function call is 25% more costly than a regular function call. In this case, the additional expense pertains only to the call itself, not the execution of the function. It's essential to emphasize this point. Usually, the expense of the function call is much smaller compared to the overall cost of executing the function. However, it is crucial to be cautious because though it may not always be significant, if you excessively use polymorphism, extending it to even the simplest functions, the extra overhead can accumulate rapidly. In C++, and in programming in general, whenever there's a price, there's a gain, and whenever there's a gain, there's a price. It's that simple.
First your whole line of argumentation about the copy overhead is ridiculous. No one will ever pass something big by value unless it is absolutely necessary, and in that case the copy should be considered part of the actual work the function has to do. If the function does a lot of work and is not called often then you eventually run into icache issues wich increases the overhead. If the function is called frequently the overhead adds up, doesn't matter that its small compared to the actual work that the function does, small multiplied by a big N is big, and if you are in a performance constrained system like a videogame renderer even small overhead should be considered. Another way in wich vtables are horrible for performance that this video doesn't mention is the fact that they introduce bloat into your data types wich is absolutely horrendous for cache efficiency, so even if you don't care at all about the indirect call overhead vtables may be a no starter to begin with. Also what "gain" do you get from virtual functions? I would argue you get negative benefits, codebases that uses this kind of thing extensively do a lot of inheritance wich is in itself bad for performance (bad for cache since you end up with HUGE data types) and just overall terrible for readability and simplicity. And even if you do single layer inheritance and you are very careful not to bloat your data types virtual functions greatly obfuscate the control flow of the program, because we all know foo.bar() calls the bar method in the foo object oh wait, foo has 5 subclasses and it could be calling any of those, so poor me running my program through a debugger will be met with a suprise when I hit that line. Readable and pretty are different things, the way you do it in C is not the prettiest but it is the more readable, in a switch statement you can clearly see how many "overrides" there are and what needs to be true in order for every function to be called. You can step through with a debugger and see everything that is happening and not have to worry about vtable nonsense that may be happening in your back at any moment. Never ever use virtual functions, they are always bad, and I absolutely mean this in absolute terms.
@@marcossidoruk8033 I mostly agree with you, but virtual-functions/dynamic-dispatch still have their place. A simple example would be a video game, where the Enemy type can be anything and should be easily extensible without changing the calling code. About the object bloat, there's an alternative you may have seen in Rust - dynamic types. The vtable only exists if the type is marked as dynamic, which means the original type doesn't have any bloat, but references to it become fat pointers (pointer to object + vtable).
@@marcossidoruk8033 To be fair when you have a switch statement, all possible types are known at compile time. When you use vtable you can, for example, add types from other dlls and it'll still work. These are different functionalities Also I feel like languages should introduce such closed polymorphism where virtual functions would compile to a function with a switch inside, would be kinda cool
@@marcossidoruk8033 The second you start running code where data loaded from storage influences the order of execution (very common in a game engine), the control-flow argument goes out the door. Yes you can still mitigate some of the chaos away by processing the execution graph into a more structured form - but some will remain anyway it will never be completely predictable. A much worse offender of control flow obfuscation than vtables are collections of asynchronous lambdas (jobs) processed by a multi-threaded worker queue - that makes me pine for the simple days of a running a method on a collection of base classes with vtables.
Remember to mark your polymorphic classes as final if they are the final derivation! That lets your compiler optimize virtual function calls into regular function calls in situations where there can be no higher superclass.
I get your point: virtual function have a cost and it's true. But the video is not 100% honest. The c++ code can be extended by only creating new type of operator without touching the code that execute operation. The C code cannot, you must change the central switch case and the enum. The virtual function has a cost but also offer functionality. Do the functionality worth the cost? Maybe yes maybe no. Each case is different and must be evaluated. Also c++ != virtual function and inheritance. It's not because the feature exist you must use it. They are tools in the toolset, nothing more. Then you also did an useless operation the force the c++ to pass by the vtable with the line "Operation *op = &add;". If you just used "add.execute();" directly the compiler you know at compile time what function to call and would not pass by the vtable. I understand you did that to have a one page example. But it could lead someone to think an example this simple would always use overkill feature like the vtable. It make look c++ dumper than it is.
I agree. In fact, it's probably zero cost if you use templates (at least in Rust). In this case, he is using a pointer (in Rust, it would be `&dyn Operation`, the dyn makes it clear it's a fat pointer)
@@nero008You do know that the vast majority of useful code needs maintenance well into the future, right? Not to mention that this sort of thinking almost always falls under the umbrella of premature optimization. Outside of very select circumstances, the performance difference between a polymorphic implementation and a monomorphic implementation is utterly imperceptible to the end user, but the difference it makes to the structure of the code could be very well noticed and appreciated by any programmer doing work on that code
@@nero008 You certainly didn’t read that way, given that you were responding to a critique of the video’s lack of nuance that makes clear that there is room for evaluating whether a virtual call’s performance penalty is worthwhile in a given case. I don’t see any reasonable way to read your response except as a complete dismissal of the more nuanced view.
I've been coding a game engine from scratch for a few years now, and in most real scenarios I encounter, where functions perform actual work, the virtual call overhead is just negligible. I tend to avoid using virtuals in a hot path (functions that will run many times per frame), but I'm totally fine using them elsewhere. This is how I dispatch game system update and render calls in my framegraph, for instance. My game systems must have state, and the engine can't know about the specifics of client-side game system implementations. All it knows is that some will modify a scene (update), and some will push render commands in a queue when traversing a const scene (render). So polymorphism is a good tool here: it makes the API clear enough, it makes development less nightmarish, the abstraction it introduces can be reasoned about, and the call overhead is jack shit when compared to execution times. Guys, don't let people decide for you what "sucks" and what is "nice" or "clean". This is ideology, not engineering. Toy examples like this are *not* real code, measure your actual performances and decide for yourself.
Yep, you can believe in whatever programming ideologies you want, but it all goes out the window when you are in a real world situation and actually need to get something working.
I can't find a source on this, but I remember reading around a decade ago, when I started uni, that the OGRE engine was made the way it is was partially to demonstrate that the overhead of virtual calls is neglible.
$70K for something you can otherwise learn online by yourself? Nah. Plus modern college is predominantly online anyway. Professors don't have time to answer intricate questions and tell you to google things anyway. Absolutely pointless.
It's always worth noting that when Stroustrup says that C++ obeys the zero-overhead principle, that in NO way means that the abstraction itself is free. It's just that you couldn't hand-code it better yourself with less performance overhead to use that feature (ideally). If you use inheritance with virtual member function overrides, you *will* pay a cost, because if you care about performance 1. you should always measure it, and 2. you should be aware of exactly how it is implemented. Otherwise, don't solve the problem that way. There are some cases where inheritance is quite applicable, but needless to say, it is not exactly cheap, and its depth should be minimized if it's used at all. And to quote Gang of Four, "Favor object composition over class inheritance".
@@heavymetalmixer91 it's as simple as including one or more instances of a class inside another class rather than deriving from a base class. So a class that contains objects of another class as opposed to deriving from another class. This is easier to understand especially compared to multiple levels of derived classes
Starsoup is full of shit and according to your definition "zero overhead" is actually not zero overhead. This is like killing some people and then saying "I killed zero people according to the zero kill principle because you couldn't have done any better" its nonsense. Zero overhead means zero overhead, what else would it mean? That is the abstraction costs no extra cpu cycles and that is exactly what starsoup means, he just never claimed vtables to be an example of what he calls a zero overhead abstraction. However, the whole idea of zero overhead abstractions is in itself ridiculous, no abstraction ever has zero overhead, and if it does it is probably a terrible abstraction that doesn't provide you with much abstraction to begin with.
@@heavymetalmixer91 Instead of extending the base class you put the object of that class inside of another class. That way you still have access to all of the fields of the "base" class but usually you need to write some additional code like setters and getters and method wrappers in the new class to get to those fields and methods. In case of c++ you can achieve exactly the same result by inheriting the base class without using the virtual keyword. The fields of base class are just going to be included in the structure of derived class and methods will be statically resolved at compile time (no vtable and no double dispatch). But it doesn't work that way in other languages. For example in java every method is virtual by default.
@@heavymetalmixer91 As a practical example, say you have a Bow and a Sword class, and you want to make a SwordBow class that is like a Sword and also shoots an explosive arrow out of the sword. You could do this by letting SwordBow inherit Bow and Sword, and then overriding the inherited Bow's shoot() method (function) to replace it with a new method that shoots an explosive one. With composition on the other hand, you simply tell Sword and SwordBow to both use a slash() method, and you then define the explosive shoot() method in SwordBow without overriding anything. Composition turns a 2D family tree into a simple 1D list of family members.
Mistake in 03:46: If a derived class has the same function as the parent BUT the parent function isn't marked as virtual it doesn't become virtual implicitly. The 2 functions will co-exist simultaneously. Which one will be called depends on the type of the variable that does the call. Is it base class variable or a derived class variable? Specifically if the pointer is of type base*, you assign it to an instance of derived class and call the overriden function it will actually call the base class's implementation of that function. This is a subtle C++ pitfall that can lead to bugs. What you probably meant to say: If a base class has marked a function as virtual then it becomes implicitly virtual for any derived class. The derived class doesn't have to explicitly mark it as virtual. But for clarity reasons it should do so.
The confusion likely stems from the default behavior in Java, where all methods in derived classes are virtual. This design makes sense for a garbage-collected language, as it can directly optimize pointer indirection. In contrast, Go takes an interesting approach: instead of storing the vtable pointer in the derived class, Go places it in the base class (referred to as an interface). In Go, each interface instance is 16 bytes in size, consisting of two pointers: one pointing to the vtable of the derived class and the other pointing to the actual instance of the derived class. In C++, however, the behavior differs significantly: 1. Virtual Methods :- When a method is declared as virtual, it is invoked based on the object's runtime type, regardless of whether the object is allocated on the heap or the stack. Each object stores a pointer to its vtable, which is used to resolve the method call. If the object is heap-allocated, there are two levels of indirection (heap allocation and vtable lookup). For stack-allocated objects, there’s only one level of indirection via the vtable pointer. While the compiler may optimize heap allocations into stack allocations, virtual method calls always involve pointer indirection. 2. Non-Virtual Methods :- For non-virtual methods, the method call is resolved at compile-time based on the class type of the object. If the method accesses non-static members, they are accessed through the "this" pointer. However, since the compiler knows the class type at compile time, it can often inline these method calls at the call site. The choice of which method to invoke is based on the class where the method is declared, not the runtime type of the object.
80% to 90% of code execution time is spend in 10% to 20% of the code. if your code is performance tuned so much that it's the performance degradation is due to vtable lookup (which I highly doubt), then perhaps you can say "polymorphism sucks". For 99.999% of all programs out there, that's not the case and polymorphism is a very useful feature and makes code simpler.
Yeah this is an extreme example because in this case, the polymorphic function in question is just: int add(int a, int b) {return a+b;} And yeah, if you're doing a bunch of one line functions it'll probably make your polymorphism overhead seem really bad. Polymorphism isn't designed to optimize toy programs, it's designed for large projects.
You say 99.999%, and your average programmer unhappily employed doing webdev might be able to agree. The problem arises when these techniques and philosophies seep into applications programming (or worse, embedded) where the bottleneck is no longer some disgustingly lethargic network activity. Then your 99.999% becomes merely "sometimes." Are 99.999% of all web-free programs I/O bound? No, not remotely. And for any of them that aren't bound by even disk I/O (nevermind the abyss of network I/O), a benchmark between polymorphic and non-polymorphic solutions will show a non-trivial difference quite a bit more frequently than you're claiming. That small bit of code you mention that occupies the most runtime? That might be a tight logic loop or some recursive tree/graph-walking thing, which isn't that unusual for a program that's algorithmically interesting. And yeah, you'll absolutely see a difference if that logic is running through some enterprise-grade polymorphic mess. So it's truly unhealthy that OOP and web/enterprise laziness, which is excusable in that one specific realm, has infiltrated universities to become embedded in the minds of all graduates, when only *some* of those graduates are going to be employed writing code that's already so slow that polymorphism is just a drop in the bucket. (And yes, I do understand your point applies to these performance-relevant programs wherein the bulk of the computation time lies within the bodies of functions rather than straddling calls that may or may not be subject to a vtable lookup, but if you want to make that argument more effectively, you'd need to shift that rectally-extracted statistic from 99.999% to something more like 80% to avoid being too obviously hyperbolic)
@@delphicdescant True. Recently was doing some benchmarking on array performance in C# and... let's say that [y*width+x]!=[x,y]!=[x][y]. And none of those is the same as objects encapsulating them. And none of the former is the same as accessing said objects through an interface. That said, such is the cost for "generic not specific". But... if you're laying tracks for a trolley car, you have a LOT more tolerance than if you're laying tracks for a bullet train. People just need to be taught "when is which". And I bet most will want to stick to either side of the fence in their work. "Application coders" and "performance coders" are intrinsically different, but both have their place because business needs vary.
@@taragnor its designed for projects that dont require hyper performance, if your project is big but needs high performance then you will start needing to not use these abstractions depending on how much optimization you need and where the bottleneck is.
"polymorphism is a very useful feature and makes code simpler" - do you have an example of code made simpler by polymorphism, or are you just repeating the marketing ads of OOP people?
This video shows a only a compaison when C++ virtual approach is disadvanaged and dosen't count the disadvantages of the C switch approach, using that to arrive at a simplistic conlcusion "virtual bad". The example show in this video the number of operation and the operation itself are very small, so here the comparison is "dereferencing a virtual table and call a function" vs "call diretly a slightly bigger function". But if we pass in a more realistic context, where there are more derived classes and bigger method, now in the C approach we have bigger switch that've to call other functions so that means that we've to do 2 function call in C approach insthead of 1 like the C++ approach, with all the overhead that comes to (or put all the logic of all cases in one function and have a massive switch). And THE CACHE MISS CAN HAPPEN EVEN IN C APPORACH TOO, you know the switch has to be loaded form memory too, like a vtable. The C switch has the advantage of being easier to optimize and can be faster, but you've to now and implement the call for every type of operation in the base class. This means that it's not possible to use in situations like extending the functionality of an already compiled library. And last which compiler and flags were used?
You can write OO code in C. C just doesn't do the heavy lifting for you. You can write your own dispatch tables with function pointers. There are plenty of OO libraries for C. GTK+ and Xt are two examples. The code doesn't look as nice, but OO is about how you organize and reason about your code, not about whatever syntactic sugar your language gives you.
While all of this is true, I think the main point of the video was that type erasure is just not a good idea in general for a staticly typed language from an efficiency pov, so c++ making it easier is a debatable choice(at least in nowadays perspective, I'm not suggesting that Bjarne should have looked at his crystal ball to see that in the future the additional level of indirection would mess with the branch predictor or the icache)
Right, OOP is a paradigm, not a language feature. C++ used to originally just compile into C first before it got its own compiler. You can do almost anything in C, it just may take a godawful amount of boilerplate that other languages will handle for you with ease.
You are bluring the line I drew, so allow me to blur yours... In Java and DotNet, and even Basic, there is/are intermediate OP Codes, MSIL for DotNet, ByteCode for Java, and P-Code for BASIC. It's possible for C to have such 'intermediate' Op codes as well, which is the Assembly language that programmers would write to if not for hi-level 'syntactic-sugar' languages... The fact that C doesn't use OP codes doesn't change the fact that a programmer COULD write Assembly targetting their CPU. An internally, the C compiler DOES have platform-agnostic OP codes. The difference between C and C++ is not just 'syntactic sugar' it is a feature, just as C is a feature, and not just 'syntactic sugar' over OP codes.
I teach college C/C++ and I think the most important part of introducing C++ is explaining the problems that the language was written to solve; they were problems with large scale software development, not writing faster algorithms. Projects were becoming really hard to manage with library name conflicts, simultaneous changes to common code stepping on eachothers toes, human problems like that. Polymorphism lets developers build on top of eachother's code in a way imperative code can't support. Someone could subclass your calculator, add/remove/change operators, and pass the new calculator into existing calculator-using code with no change to your calculator class code and just instantiating a different class in code that depends on your calculator.
Indeed, it's a common theme with these UA-camrs that they take very simple coding problems and think they can extend those experiences to all coding problems, but when you are not alone working on your own codebase and instead of a team of half a dozen, or god forbid dozens of people and you don't implement polymorphism you are going to grind progress to a halt needing to rewrite a bunch of code and any new members to the team need to first spent weeks or months studying the entire codebase...
You made me think of C#. I appreciated the interface there, and realized quality GUI frameworks require OOP. There are these use cases as important as selecting the right language for the job at hand. If you make a backend or low level dev look at some AI models, they will flip out. No, go ahead and do that functionally. I'll check up on you in ten years.
This video is misleading. Even if the vtable overhead affects the runtime of a time-sensitive program, this video misses the point of polymorphism to begin with. In most cases where dynamic dispatch is preferred over static dispatch (e.g., game scripting, networking, client programming, GUI apps, etc.), the overhead created by vtable accesses is negligible. You would lose more processing time in a program that statically generates similar functions for hundreds of classes and the various functions that take those objects as parameters than if you were to use polymorphism for said objects. There are different applications for both static and dynamic dispatch, and it is misleading to hold them to the same expectation.
In large project, polymorphism could literally save tons of amount of time and be really helpful to organize the code. Things like OOP, design patterns, are basically just for large projects but not for something small like calculator or some kinda stuff.
Thing is, they don't always make your code easier to write. Using polymorphism often comes from the assumption that your code has to be generic on order to be future proof and easily scalable. But the reality is: very rarely do you actually know whether you will need this extra scalability in the future, ESPECIALLY in large projects. Using stuff like enums, or sometimes type erasure doesn't have to be that complicated (I mean enums are really simple, seriously) and they can minimize the performance overhead. Polymorphism can be okay, if you KNOW that you need it for any given task. If you're just trying to plan ahead chances are you will mess it up because no one is that good. Maybe it's okay when you only have a few polymorphic classes with a single inheritance level... but when your WHOLE API is almost exclusively polymorphic, with sometimes 3 or 4 levels, it's a nightmare performance wise, for a minimal benefit
@@eddiebreeg3885 Agreed, if you try to use polymorphism on everything you are going to have a bad time. I look at it more as a tool for very specific things. Any feature of a language can be bad if used without care.
The thing with OOP is it's far more flexible. Imagine you wanted to add a new operation. Using the C method, you'd need to be able to directly alter the original source code. If you're using an external library, that may not always be an option. Under C++ polymorphism you can build a library that can be extended with new operations simply by extending the base class and you never need to alter the base code. And that's one of the main advantages of OOP, being able to create libraries of classes and functions that people can just drop into their programs and use.
I'd say that polymorphism doesn't necessarily suck. Poor use of polymorphism sucks. If your virtual function takes only couple of cycles to complete, then it might not be a good idea to use polymorphism, but when it takes couple of miliseconds, the slowdown caused by the extra lookup is negligible.
You explained very nicely why polymorphic code is slower, but it sucks only if you need to squeeze all possible performance from the hardware by a specific task. Which is the case pretty rarely. The most common scenario is something (I mean, most of the code) is waiting for something slower, meaning it has more than enough time to not create any measurable lag. But yes, when you're optimizing a tight loop and every cycle matters, then the good old C is the way to go. But again it would be pretty tricky to provide a right example. In my practice - if I have a tight loop and I want to save every cycle - I just avoid calling anything, inlining as much as possible. And this can happen inside a C++ overridden method, but it just doesn't matter as long I don't call other other methods from said tight loop. Sometime I mix C with C++, where C++ is for general app logic, and C is doing time critical things. The common C++ usage is for tasks too tedious to code in C and usually not time critical. Like UI and network. The code waits "ages" for something to happen, then you invoke a C function that does the heavy lifting pushing gigabytes of data because the user just moved a mouse or clicked a button.
Yeah the title should have been specific for C++, instead he chose to clickbait and generalize it to all code, which is not the case. If you need to develop software with a team of people not using polymorphism will cause more problems than the potential performance loss, which in most real world cases that aren't basic C++ scripts running on Arduino don't exist in the first place.
This is a nice video but I think it's also a bit misleading... The C code isn't *really* polymorphic: Every 'method' added has its own switch statement, every 'child' added is a new case for every switch in every function and the monolith grows. And the C++, of course it'd be silly to use that kind of indirection on the hot path (and std::visit(); might be preferable). Either way, the compiler will optimize away where possible for the target platform, including switch statements.
Polymorphism doesn't need to involve dynamic dispatch though. In Rust, for instance, polymorphism can be achieved with generics. And the cool thing about generics is that at compiler time they get monomorphized, i.e. the compiler will just copy-paste the function with every combination of concrete types that it's called with, so you just end up with roughly the same setup you had in C with no performance cost. I haven't had much experience in C++, so I don't know how templates work there, but presumably it would be possible to do the same thing there. Also there are situations where dynamic dispatch is needed, so, if you're writing it in C you'd use function pointers and it would be just as slow as virtual methods.
Incredibly misleading. You're not comparing the same things. Full-fledged polymorphism with virtual calls cannot be compared to a case over an enum. If you were comparing C++ to a function pointer call (with the destination of the pointer compiled in a separate TU and the program compiled without link-time optimisation) in C, that could be considered "comparable" at least.
some might say that tools that are easy to misuse categorically suck more than tools that aren't easy to use at all, though that wasn't his point, so that wouldn't help his case.
I mean this guy just doesn't know what he's talking about, which is rather apparent. This whole video carries the same argument as "random access data structures suck! they're slow!" when all he's doing is performing tons of insertions and deletions at the middle of the list. He takes 1 example and compares it to another completely abused case of a language feature and draws the conclusion that it "sucks".
@@justaboy680 E.g., the claim that because C is not an OO language, you cannot write OO code in it. Of course you can. What do you think fopen/fread/fclose do, if not return and work with an object handle with virtual member functions?
C can be written in OO style (see GObject), it just requires you to manually implement the pattern. An interface in C would be a structure containing function pointers constructed by object specific functions.
For such simple operations vtable lookup represents 20% more of execution time but truth is in a real program it would not represent such a high proportion of execution time. And C code might be less easy to maintain down the line
I LOVE polymorphism. Let me tell you why. I've used it in providing an undo/redo feature, as well as in writing an arithmetic parser. It elegantly separates the overall design from the implementation of nodes. So if I have to fix nodal behavior or add new node types over time, I don't have to touch the top level architecture. It. Just. Works. Yes, there is a small performance hit but it's more than worth it to me to have more readable, more maintainable code. That said, one alternative is to use name overloading (what I like to call "static polymorphism"), which is resolved at compile time and solves the performance hit.
You don't need to recompile the binary containing the base class when you need to add another child class. You can swap functionality on linking stage. You can plug in different implementations without recompiling the core logic. That's Open-Closed principle for you - the parent class stays closed for modifications, it literally never gets recompiled anymore, but open for extensions - you can add child classes freely and compile them separately.
I used polymorphism in writing simulations where the code paths were things like "run this CFD code that takes 20 min" vs "run this approximation that still takes 30 seconds". It made for convenient organization where the child classes would track whatever extra data they needed. The overhead in using virtual functions maybe added a few millionths of a percent to the overall runtime of that particular feature.
About the V-Table, if you use the final specifier on virtual functions this(amongst other things) provides the compiler an opportunity to "devirtualize" the function.
Well, this has been discussed dozens of times already. Essentially the example you constructed is specially crafted to make polymorphism look bad. If you want to do a simple calculator, the operation can be done in a single instruction and fits in register. For the types of code that OOP was designed, though, it has to search data structures which use memory allocated in the heap, fetch data from storage, invoke kernel syscalls, etc, and all of this will make the overhead of calling a vtable negligible. And even in the case that you find yourself in a situation where you need to have the CPU call an operation millions of times you could just dump the class structure with other code, translate it to C style opcodes and run this instead. This is what any numerical library in a virtualized language does, btw. And if you do your calculation in the methods it provides, instead of using the language directly, it's just as fast as C code. At the end of the day, the only performance penalty you cannot get away if when using higher level abstractions is the start up time it takes to initialize all the stuff, and a fixed amount of memory to store everything. I definitely think yoh could provide better content to your viewers than something which already has been exhaustively discussed in the 80's.
Firstly, I'd like to emphasize that polymorphism, like any tool in the programmer's toolbox, has its optimal use cases. It's not inherently 'bad' or 'good', but should be used when appropriate for the problem at hand. An overly narrow focus on performance can inadvertently lead to premature optimization, and it's important to remember the classic Donald Knuth quote: "Premature optimization is the root of all evil". Polymorphism shines in scenarios where you need to process a collection of objects that share a common interface but have different internal behaviors. In these cases, it allows you to write more concise, readable, and maintainable code by avoiding verbose switch-case statements or if-else ladders. Although polymorphism introduces a performance overhead due to the indirection through the vtable, this is often insignificant in comparison to the cost of the actual function execution. And while it's true that in a tight loop this overhead can accumulate, this isn't where polymorphism typically brings the most value. To your point about how vtables are implemented, it's true that there's no standardization across compilers. However, most C++ compilers adopt similar strategies, and the vtable is usually stored separately from the object instance, with a pointer to the vtable added to the object's memory layout. This additional level of indirection can indeed add to the function call cost, and that cost is further compounded by the challenge for CPUs to predict dynamic jumps, as another commenter rightly pointed out. I also agree with the critique about the video's simplistic example. A more complex set of operations would have made the comparison more meaningful, as the benefits of polymorphism become more apparent when you're dealing with larger codebases and more complex behavior differences between classes. Finally, it's worth noting that C++ offers more than one way to achieve polymorphism. Beyond the classical (runtime) polymorphism we're discussing here, there's also compile-time polymorphism, also known as static polymorphism, via templates. The latter can eliminate the runtime overhead, at the expense of potentially increasing the binary size. It's yet another trade-off to consider based on the specific needs of your program. Overall, I think it's crucial not to dismiss polymorphism out of hand because of performance concerns. Instead, we should understand its costs and benefits, and use it where it makes the most sense.
@@T0m1s Well if you had read a bit further than just the first few lines you'd see that I'm providing actual examples, starting at "Polymorphism shines in scenarios...". I will forgive you though since attention span is a common issue these days. Below is another example that will further elaborate this. Suppose we have a number of distinct shape classes, such as Circle, Square, and Triangle. Each of these shapes can be drawn and resized, but the specifics of how these operations are implemented vary between each shape type. By defining a base Shape class with virtual 'draw' and 'resize' methods, we can implement polymorphism. Through this approach, we're able to maintain a collection of Shape pointers, without needing to know the specific type of shape they point to. When we need to draw or resize a shape, we simply call the appropriate method. The correct implementation is chosen at runtime based on the actual object type, thanks to the magic of polymorphism. This not only makes our code more concise and maintainable (as we're not wrestling with unwieldy switch-case statements or if-else ladders), but also more extensible. If we decide to introduce a new shape type into our program, we simply create a new class that extends Shape and provides its own 'draw' and 'resize' implementations. No need to touch existing code. Polymorphism thus allows us to write code that is open to extension but closed for modification, embodying a key principle in object-oriented design, known as the Open-Closed Principle.
@@T0m1sIterator objects. Also definining common interfaces for data structures. For example you can have a Collection class that defines a search(x), insert(x) and remove(x). Then when you have an object with the type Collection you know you will be able to call those methods, ignoring the actual data structure used under the hood. A pseudocode example could be: class Company: def getWorkers: Collection then you can perform c = Company(...) ... w = Worker(...) workers = c.getWorkers() workers.search(w) You can ignore how the Company class stores its workers (it could be a hash table, a double linked list, a Tree, etc..), all you care about its that it will return a Collection.
Nice video! There's a miss at 2:15: C++ classes are not C structures with function pointers. That's true only for polymorphic classes - and not even for all types of polymorphism, if we have to be picky. C++ classes are basically just structures, with member functions being normal functions with an implicit "this" argument. It's also worth mentioning the use of the "final" keyword can help assisting the compiler in optimising away virtual table lookups when virtual functions are called via the final implementation. Still, I prefer the C way as it doesn't add hidden data members and doesn't perform the shenanigans I described in this comment. Much easier to reason about.
Function with the same signature in a derived class is not virtual by default in a base class as stated somwhere around 3:40. You need virtual keyword in a base class to enable dynamic polymorphism. It's not only for code readability.
To be fair to all those C++ code bases littered with virtual functions and interfaces: With C++ not having a clean way to define restrictions (SFINAE my ..) for generic/template parameters until C++20 (and I have not actually seen concepts used in any productive code base to this day..), it was often the easiest way to get at least somewhat sane compiler messages, even though static dispatch or an enum style solution would have been perfectly possible for a lot of these usecases. Rust (btw) does it kinda cleverly by combining dynamic and static dispatch into Traits, although using trait objects is somewhat frowned upon at times.
In C++17 you can use std::variant and std::visit to do static dispatch with clean type like Rust enum. C++20 concept is syntactic sugar for my point of view. Almost everything can be written and read easily with constexpr and static_assert. And SFINAE can be used to test the existence of an expression and transform it into a trait (a la std::is_arithmetic).
I don't know why you are saying that trait object are frowned upon, lot and lot of widely used library abuse it, like tokio for exemple. you dive into any async library and you will see Pin everywhere (often aliased by BoxedFuture), it is just so much easier to work with.
C++ as a language, is capable of solving the problem introduced in the video faster than C contrary to the argument. If you need to have a “dynamic” operation (like a custom operation for a mapper or a predicate for a filter), on C you are stuck with function pointers (the given enum example is unrelated to the problem space IMO, it is fully static and not bound late at execution time) and they cannot be easily inlined by the compiler as they are only known very late at execution time. C++ OTOH, is capable of doing the inlining at compile time thanks to the superior type system. Overall I think this problem is a pretty bad example for polymorpism. It is just not the correct tool for this use case. Of course there are many ways of achieving the same thing and some of them will perform slower and it is something to consider when picking a solution. For the provided example, a compile-time polymorphism strategy (ie changing the operation at compile time) would perform the same as the C version provided. Note that the C version would not be versatile as you cannot simply provide the operation at the callsite and you’d need to modify every place you want to use that operation without a function pointer.
Fun fact: doing all of that stuff in C is completely feasible, altough it's unconvenient (lots of boilerplate code is necessary, C just is not object oriented) and unsafe (you need to do type checking at runtime). I actually discovered these things on my own a few years ago, when I first began studying OOP and tried to replicate it in C, which was the only language I was familiar with at that time. When you actually implement this stuff, you realize how much wasteful it is. Btw, please note that there are many types of polymorphism. This is specifically what's known as inclusion polymorphism, which as far as I know is the only kind of polymorphism which has to be implemented with these type of runtime mechanisms. However, polymorphism is not something which is exclusive to OOP and stuff like adhoc polymorphism (ie function overloading or even traits in Rust) and parameter polymorphism (ie templates) are usually implemented statically. I know that there are a few other kinds of polymorphism, but I don't know much about those.
Good point, but in the calculator example, the C code has conditional code that the C++ code does not have. Believe me, after having programmed in C++ for 30 years, I can assure you that the supposed overhead of polymorphism is negligible in the overall execution time of a real world program.
Depends on the specific case. If you loop over 100 million elements and call an add function (virtual or otherwise, as long as it’s not inlined) in every iteration then it’s way heavier than just summing the numbers in the loop. As always, it’s about choosing three right tool for the right job.
@@maxp3141 Ok, but this is why I specified “real world program”. I have no experience of real world programs that loop a million times calling a function that merely performs a sum. Functions are typically more elaborate than this, so I think that my reasoning is sound.
The basic point you're trying to illustrate about the vtable is correct, but I think we have to clarify a few things. First of all, this is not about C++ but about the code style your're using (runtime polymorphism). If you hadn't used a manual way of creating a function pointer, the compiler would most likely have resolved the polymorphism at compile time. Speaking about compile time, you could also use templates for enforcing compile-timepolymorphism. And modern CPUs are good at resolving indirections like vtables, too. All in all, OOP and polymorphism can be great for structuring code, but I think they're overused.
It's not always about performance, you have to choose wisely what you want to use for your particular use case. If your use case requires raw performance, OOP may not be the way to go. But if you don't want to spend 5 hours debugging your C code, maybe OOP is the way to go. Still, it's very nice to see someone explaining the implementation details of C++. Great work!
Why would a procedural code be a mess? It's usually really flat, as opposed to OO code. Which in my experience makes it a lot easier to debug, there's a lot less to worry about overall, functions are the only abstraction mechanism. Sure C++ compiler is better, which why people write C-style C++ over C.
"if you don't want to spend 5 hours debugging your C code, maybe OOP is the way to go. " - OOP changes 5 into 50-500 hours, which is good for job stability
I only use virtual functions for my callback classes, it's nice to have syntax that wraps up functions in a single class and doesnt need to use C's terrible function pointer syntax. Never heard someone using it for "debugging" reasons
I think that the concept of this video is wrong from the base. I mean: polymorphism sucks... if you need performance. But when what you need is code easily maintenable, and the performance is not a problem, it is a good tool (but don't forget what happens when you only have a hammer...). If performance were the only metric, we would use only assembler/C, and never languages like python, perl or java.
The c code in the video already looks so much uglier than the c++ version. Just imagine building huge libraries such as image adapters. It would look like spaghettis for sure in the C code.
This is a mood point: sure if your method contains only one line then yeah, it will be slower but if you write code like that, you either aren't a good coder or you are writing java ;)
Hence why devirtualization is a big subject for compilers, making this problem disappear when possible. Using sealed classes at the end of the inheritance chain helps for devirtualization.
Enjoy your videos, it's great to see videos explaining the inner workings of the stuff that we just take for granted now. I think you may have miss spoken a couple of times around 3:32. The "=0" is not needed in order for the compiler to create the v-table entry, this just indicates that the class is only to be derived from and that the derived class must implement this pure virtual function. Non-pure virtual functions (without the =0) need not be implemented in the derived class if the base implementation is appropriate. Also you say that the virtual keyword is not needed and that by simply declaring a function with the same signature in the derived class the compiler will assume the base method to be virtual. I think this is true in some other OO languages, but not in C++. You cannot make a base class virtual without specifying the virtual keyword in the declaration of the base class. It is true that you can declare a function with the same signature in the derived class - this is know as "overriding" (which is not the same as "overloading") - but it will not get an entry in the v-table and will therefore not get called by a method invocation from a base class object. The following g++ program demonstrates this: #include class base { public: void identify(void) {printf("%s ", __PRETTY_FUNCTION__);} virtual void virt_identify(void) {printf("%s ", __PRETTY_FUNCTION__);} }; class derived : public base { public: void identify(void) {printf("%s ", __PRETTY_FUNCTION__);} virtual void virt_identify(void) {printf("%s ", __PRETTY_FUNCTION__);} }; int main(int argc, char *argv[]) { derived d; base *b = &d; b->identify(); // calls base::identify() (not a virtual function) b->virt_identify(); // call derived::virt_identify() (via vtbl) d.identify(); // calls derived::identify() (overrides base::identify()) d.base::identify(); // calls base::identify() (explicitly) b->base::identify(); // calls base::identify() (explicitly, bypasses the vtbl) return 0; } Program output: void base::identify() virtual void derived::virt_identify() void derived::identify() void base::identify() void base::identify()
OOP is just a tool. It will be great if you know how to use it. PBRT use very much OOP a lot. It really difficult when I tried to port their code to Vulkan and GLSL, but then I realized why they choose to use OOP.
The real problem is actually the loss of a lot of function inlining opportunities. Even with a switch table, the compiler would be aware of the complete set of behaviors and can inline the function calls. The situation is even better with compile time polymorphism like with CRTP, which often produces the most optimized binary. But with a vtable the compiled binary has to remain compatible even with derived classes made later on that can arbitrarily overload the methods. Devirtualization can sometimes overcome this problem if classes are declared final, but it doesn't work that well and also coders often don't bother marking classes final.
std::function, lambdas, concepts etc. v-tables and virtual are rarely actually required and usually just a convenience tool for doing something in a specific way.
7:04 you can always do these code optimizations when using your own code or have access to source code, but when it's in a library form you just have to use what's given.
Рік тому+1
Also good to note the performance impact of branch prediction misses. In normal code, the CPU is trying to predict whether a jump is going to happen, but it always knows the destination of the jump. With virtual functions the CPU knows that we will be jumping but it is trying predict the destination of the jump because the address is loaded from memory. When a branch destination is mispredicted, the CPU first needs to load the instructions for the correct branch, and only then it can start loading any data the instructions might require. The performance is also dependent on how predictable the function usage pattern is.
3:40 did you mean function in derived class which have same signature and name as the one declared `virtual` in a base class? In derived class can omit `virtual` keyword but in base it is mandatory. Otherwise, you will overload but not override. Since C++11 best practice is to use keywords `override` and `final` which are written at the end of declaration. Function which does not have `virtual` keyword in base class is common method, not virtual. C++ does not have "virtual by default". And your comparison "fast C code" with "C++ slow code" is actually unfair. C++ developer could write same "C code" if needed. Polymorphysm (or other sorts of type erasure) is a tool which should be applied wisely. Ex. You are sure that two pointer dereferences are neglectably small compared to work done in those virtual functions. And your code is more maintanable with virtual actually. (Ex. end.)
Respect for taking the time to write such a critical video. A suggestion I make when writing critical things about a programming language is to let clear when the criticism goes on the language itself (e.g. syntax), and when it goes on implementation aspects. Your criticism is on an implementation aspect, that can be optimized by a compiler. So, if you think well, your criticism isn't on (C++'s) polymorphism.
this is purely theorical. in majority of real world programs, the actual function is so much more packed with memory lookups and operations that the single additional pointer dereference's effect on the final performance is insignificant. on the other hand you get a clean and maintainable code. weighing both sides of this scale, my choice personally would be rarely different. in other words, "polymorphism sucks" is very big claim to be fully settled with a 25% performance penalty on a function that has no parameter and does a simple mathematical operation.
1:35: WRONG! Just because a programming language hasn't first class constructions for object orientation, doesn't mean that you cannot program it in a true object oriented fashion. I have programmed object oriented in C. Not easily, nor with any pleasure, but it is possible: in every struct you put pointers to the class struct that contains pointers to the methods, of which the first parameter is a pointer to the "class" that you have defined, a parameter that is normally hidden in the most programming languages, except curiously Python. You can also make pointers to the superclass instance, or implement it otherwise as field copying.
It's the execution pipeline stall/flush/reload that burns the most CPU, not the vtable lookup. However, polymorphism can eliminate huge swaths of complexity in large programs when used appropriately. It's just a tool. Not a panacea
Not you but there seems to be a few people on the Internet who either hate OOP or are just obsessed with raw performance, and this is usually marked by the extensive use and emphasis of edge cases. In the real world getting working, maintainable code out of the door quickly is far, far more important. If it takes 0.2 seconds longer to execute then most of the time it simply doesn't matter. Good video though.
"C++ is just a bunch of compiler tricks" - old CS prof. There was a time where C++ was just another pre-processor to a C compiler. Most of the useful parts of C++ can be had in C by using an instance of the "object's" data as the first parameter to each of the "object's" methonds (functions).
I think virtual functions should not be and frequently arent used in hot spots of the code, they are more proper to handle choosing different broad code paths (choosing between exporting to jpg vs png, serial console vs telnet)
Do we get the same performance degradation if we use statically allocated 2D arrays? Because I think the vtables are exactly that - they are pre-computed. An interesting comparison would've been creating your own vtable-style arrays in C vs. C++ virtual functions. But yeah, polymorphism is very much overhyped. It does have some nice use cases, but I love the freedoms given by hybrid languages like C++ and python.
Depends on what you mean by "statically allocated 2D arrays". If you were to embed the full vtable in the structure itself, you would waste a lot more memory, but gain performance because you would only need to look up a single function pointer in the structure itself. If you only store a pointer to the vtable in your structure, it would be exactly the same as in the C++ implementation.
You need to keep in mind that historically C++ vtables were better on older systems then they are now, modern systems have made it harder to justify their current existence except when the inner elements of your virtual table functions is the hotpath. Also final pretty much makes them almost as fast as the function call because they get "devirtualized" by the compiler.
you can do OO in C and it would be true OO. All you have to to is the leg work that C++ compiler does on your own (MACROs as very useful in that regard). GObject would be a very good example of that (if not the only one?). A good idea? IMHO no.
In modern C++, one could use a concept for polymorphism, but in Rust switching between a v-table based dynamic polymorphic dispatch and a template based static dispatch is much easier, and with no wrappers.
I don't think overridden functions are virtual by default. Unless the specs have changed. If you override without using the virtual keyword, it only uses static analysis to determine with method to run. I.e. no v-table.
I think the title is misleading: Polymorphism is more than just inheritance. A different type of polymorphism is using generics/having type arguments. In Rust, this complexity is resolved ("monomorphized") during compile time, removing the overhead that inheritance would have in this instance. Of course generics are pretty different to work with than virtual methods, but "polymorphism" was the chosen word. Another example could be using interfaces (dynamic dispatch) and traits, that can also be considered polymorphism and in certain languages (say, rust) also have no runtime overhead. A title that would be less of a problem in my opinion could be "why does inheritance suck for runtime performance?", or "why does inheritance suck?" for short.
A single virtual call is essentially free, it's just a pointer indirection. It's when you have many many calls that you start to see the impact. With modern c++ you can use some template tricks to get rid of it at compile time in most cases
This is not saying that C++ is slower than C. It's just that this is a great explanation of why hand coded switch statements can be faster than virtual functions, both of which you can do in C++ also. Virtual calls certainly need to be eliminated from performance critical areas of your code. In this use case I would use templates rather than virtual functions to achieve the same thing with probably better performance than the C code.
I'm reading a book about C++ written by Bjarne Stroustroup (the creator of C++) and he comments this about virtual functions: "The mechanism of calling virtual functions is almost as efficient as calling 'normal' functions (within 25%), so that efficiency questions shouldn't scare anybody off to use a virtual function, where a normal function call is efficient enough."
The main problem with virtuals is the compiler generally can't inline. That's often a big part of the speed difference. However virtuals are still a good option in many places.
I appreciate how this is actually a technical look at HOW and WHY, rather than just says "this is slow and bad" Showing the ASM in an understandable way was really nice, and really helps to clarify whats really happening behind the scenes
The performance tradeoff for obeying "more readability by polymorphism" is just not worth it and there's a better perspective on the problem that can help you use other design patterns instead of polymorphism or switch cases.
It would have been nice if you had said something about static dispatch and monomorphization, which languages like Rust do by default. For those that don't know, it creates a copy of you code for each type that you pass, so you get all the benefits of polymorphism without this runtime overhead.
I've actually written a lot of OO code in C. Depending on how you write it, it can be horrific or reasonably pleasant. When I first wrote my data structures library I implemented it in a somewhat generic way with loads of function pointers and size fields and type ID's in nearly everything. It was really easy to work with, but it kind of looked fugly. At some point I implemented some standard types to use as templates, and it was huge. Not that it ran particularly slow or anything, or produced bloated binaries, but it was a lot to maintain. For the second version I just said "fuck it" and implemented raw data copies and had the user provide a void pointer and a size. Let them figure it out.
I suppose you could always try to emulate how the C++ would implement vtables in C. The indirection will always be more slower compared to enum. edit: try godbolt next time
This omits the fundamental advantage of polymorphism, which is that you can call methods on interfaces without knowing all the possible object types, and extend the application without recompiling. For example, you can have code that calls a method, and the object it is called on is provided by a shared library you load at runtime. The implementation may not even be written at the time that you write the code that uses the interface. This flexibility comes at a cost. Which is why code that doesn't provide this flexibility can run faster when that flexibility is not required. But try to provide that same flexibility in C, and you end up copying the same vtable mechanism C++ uses, only you have to do it manually instead of the compiler doing it for you. Polymorphism does not suck. It is a powerful tool, and as such it must be used with care. Learn when to use it, instead of overusing it or swearing it off completely.
It's not polymorphism that sucks, it's this video. You just don't use polymorphism like that, unless you're a complete beginner learning programming in the 90s. You don't litter your code with useless class hierarchies. Oh, and you can write OOP code in C, even with something like vtables. It's tedious and ugly and you just don't want to do it (particularly since there's C++ which does it much better).
Small point: Shouldn't there be more trials involved in this kind of test than one, you know, for science and stats and all? Unless that shell stuff involved running the program several times, I couldn't quite figure out what was going on.
I think one misconception is that object-oriented or procedural code is tied to a language. Part of it is the marketing of the languages. Ultimately, all code is compiled to ML even if it is through multiple layers it has to eventually be compiled to ML. No one will say that Assembly (which represents a 1 to 1 with ML) is object-oriented, but the object-oriented code is still implemented in Assembly (or ML). I can write code that is completely procedural is Java just by putting the entire contents of the code in static methods call by the static main function. The code is written in an object-oriented language and has at least one class; however, it is not object-oriented code. Same with C, I can package the data and functions together and use function pointers to create object-oriented code despite the fact that C is a procedural language. Object-oriented, procedural, functional, etc. are programming paradigms and languages can facilitate the use of one or more of those paradigms, but with the exception of a language that completely enforces the use of one of those paradigms (which C does not), they can be implemented using most of the languages out there.
Well, no wonder you think it sucks if you use it that way. I believe that's an overuse. The key point of polymorphism for me is the fact that you can hide implementation and its dependencies, for example to third party libraries, to not pollute the business logic with that third party library and to make that functionality mockable.
Why do programmers always give such random names to features in their languages: “polymorphism”, “lambda function”, “heap”, “stack”, “mutability”. So esoteric and undescriptive. And don’t even get me started on their affinity for acronyms and abbreviations for everything.
I am a hobbiest programmer specializing in nlp. Classes and python are a godsend. The one thing holding me back usually is getting the code down and polymorphism and computionally heavy(as in needing more compute to compile and interpret) are way better for me because i am the bottleneck more than a computer. Also i cant run c code on google colab.
We can just use templates which can be used for compile time inheritance basically And with the addition of concepts it can be done without gouging your eyes out 😂
It's unfortunate how a lot of videos/resources treat polymorphism and virtual dispatch as synonymous. Polymorphism is a much broader topic, this reasoning really only applies to dynamic/runtime polymorphism and a poor implementation at that.
To be fair, modern (youtube) talks I have watched almost always gravitate towards implementing polymorphism via template metaprogramming techniques, which in turn eliminates runtime dispatching and compile time optimizations, thus achieving max performance.
This video explains polymorphism quite well. I just do not like how it paints it as a massive performance hit. It is just a tool to be used when appropriate, yes it can be overused but so can any tool. When performance becomes a problem i can guarentee that it is probably not polymorphism eating your processing time.
7:00 so are you saying this is only a performance problem when the child’s vtable and related code aren’t loaded into RAM? If that’s the case, if there was a mechanism to indicate to the program which vtables to load into the cache, then in theory the performance difference between the C version would be similar, assuming that the solution using vtables is similar to the C implementation of the example code in this vid.
this video is an example of "the tool X is bad because I can use it wrong on purpose if I want!" reasoning. No C++ programmer would use polymorphism in a real world scenario like this. The best case scenario I can think of is someone saw an educational code sample which had to fit on a single screen, and thought that the example is the recommended usage, not just the syntax.
I think it's a bit wrong to just say "inheritance is bad" as a blanket statement. Rather, you should be aware of the downsides and use it sparingly. Also, I've seen object oriented programming with a sort of "inheritance" done in C. Look at SDL or the Linux kernel source code and it's there. If you want to talk about, say, an input device in a generic way, they just use a struct with function pointers that point to a specific driver's implementation of some generic function. Granted, performance is *probably* slightly better than C++ because you can knock out the vtable and only have a single layer of indirection but it's still not free. The thing to takeaway is only use this when you need it.
There seem some minor errors in here: - Methods do not become function pointers, unless they are virtual! Generally they are just regular C-like functions with a *this parameter and name of functions is "mangled" with the class name so linking them at call locations is simple. - By default child classes of same name / declaration are not virtual. They just "hide" parent class functions if you are not marking them virtual! That is you can call child->operate() and it becomes the overwritten function, but if you have a parent pointer, calling parent->operate() leads to the parent-defined method (if any). This is actually very error-prone and rarely used - but no vtable exists in this case - its all compile time. So it "only works as you hope for if your compiler at the moment see the type info" VS. with virtual it really is dynamic and cuntime polymorphic. - I would call the first C example also polymorphic - it is the "tagged union" technique and there are countless ways to do polymorphism. This is one runtime polymorphic technique which is faster than the inheritance based OO polymorphism and nearly always better by default - but did not took off much, because when OO was formalized caches were not so big of an issue, but now REALLY are on literally any computer. Other polymorphic techniques exists thoiugh - for example making compile time polymorphism with templates you can write the C++ code to outperform the C codei... - There are techniques that you can make the compiler "devirtualize" your call. There might be some reasons you HAVE TO write out the word "virtual" but want the good speed. For example marking the class final and not using it through different compilation units (or not on the hot path) usually leads to the compiler being able to devirtualize it or even inline it for you. The issue however is that it takes extra work and skill to make all kinds of compilers do it for you and that it is extra mental effort. Link time optimization can also help, but I practically never use that unless in legacy code, because much better to just properly control what is going on... I would agree with most of what is in the video. Also would add that in my opinion inheritance based polymorphis should be considered as harmful as GOTO - sometimes I do use GOTO so very rarely I also use this, but very rarely and try not to.... and not just for performance, but because it sphagettifies the code honestly..
Inheritance and runtime polymorphism can be separately useful in all sorts of places, but together they can cause issues if you're not careful and deliberate with their use, even then the applied classes need to be "atomic" as in simplistic and not divisible for your use case. Unfortunately a lot of C++ developers have been around Java and C# too much which destroys the multi-paradigm nature of C++ in their mind and ruins them as developers. I don't think I'd ever consider it as harmful as goto, since goto has killed people and disrupts standard flow control and its not hard to see why it would kill as a result, (and not to mention 99% of those cases is because multi-loop breakouts are otherwise impossible, so its an oversight that has an easy syntax address) whereas polymorphic inheritance is not disruptive to the flow by its nature, if you're using it to do that, its garunteed your first mistake was not polymorphic inheritance, and most of the cost is likely to be in performance, not production quality, though if you want to consider an non-scalable development as "production quality" then sure, but I wouldn't consider it that, it would then only produce developmental hell at the worst.
@@Spartan322 I consider bloat as production quality - and this leads to a lot of bloat. - There is endless many ways virtual function based inheritance can kill in the same ways: With GOTO its also not the GOTO that kills, but sphagettifying the code so much that you cannot follow where you make a jump and into what state - and this is the key. The jump just happens and shit happens. Unless you code in assembly and goto (jmp) not to a label, but to a memory address in a register / variable, its not the goto that do the error, but the side effect of goto - that you made a mistake where things should jump or what is state expectations there and generally because of total spaghettification you do not know where you are. Now look at very deep inheritance hierarchies and patterns where you pretty much never see the whole picture ever. You cannot even tell what will happen if you call a piece of sh... I mean code. Some of this should be generally an effect of any polymorphism, but honestly I see this coming only mostly in virtual inheritance kind. - Yes you are right in most of the text, I do consider oop as bad as goto with these virtual inheritance not for the exact same reasons as I consider goto not the best tool. Honestly I had to use goto here and there (for example for a perf optimization for the instruction cache of the hot path: to separate the cold path out with a jump ahead... because relative jumps can have 8bit single-byte parameters as opcode and calls need at least 16 bits so waste more for the cold path + of course the jump way you can spare the call (as the branch you need anyways in the instruction stream). In any ways with goto it was simple to generate the optimal code that took only 2 bytes away from the hot path that would jump out to the cold - then jump back while it would took like 16+24 at the very least if I do a noinlined function call. I find this a very raw use-case. I had to use goto similarily for rare cases over the years and its good its there - but generally bad because its hard to understand what is going on if you overuse it so its not overused. The most right part of your text is what you say about people who do too much java... I also have seen people want to do java/C#/ts-like reference semantics everywhere thus their code was all with pointers and virtual functions and array of pointers and so on.... This is not only bad for performance, but very bad also for memory safety - and if one wants both, then it becomes really complicated... Then agaiin inheritance based polymorphism also create random issues when it has interplay with threading... For example I had a team I worked together with who "released the this" in an abstract base-class that I should inherit from in a plugin-based system... The issue is that in their constructor they called some shit that got to know about the this - but object construction was not finished and partial - yet the this pointer was given to some random thread in their constructor. There was no way to make it thread-safe and I had no access to their implementation source codes, just the header.... Funny enough when I explained what is happening (because I literally reverse engineered what they are doing and why I get nasty bugs) they even told me that because its interplay of my code and theirs and they cannot see it in theirs they not gonna fix it.... but omg its very bad practice to leak the this pointer like that and I even pinpointed what thread they gave it to and where it is misused before construction finishes - and this was in a plugin based thing where official docs literally said they made that class to be inherited from. This again also could have killed (it was train control system actually), so I literally rewrote their whole class after reversing it - then fixed the bug by writing a binary compatible variant that we call instead - then made it part of the build pipeline that we do not build if they change the original class........ very nasty..... with no power we could push them to fix in their own code....
@@u9vata I would distinguish between production quality and developmental quality, scalability and long-term ease of development (which is generally distinct though related to maintenance) are completely worthless to a customer generally, and they're not always worthwhile even for you to do, this is another reason it depends on the tool, sometimes a quick and dirty solution is all you're allowed and that's another advantage, though many folks look down on it, I wouldn't discount that fact and I've definitely seen cases of virtual inheritance being adequate for short term development, in some cases speed and memory usage aren't a problem, its pure development time and functionality, so long as the user can reasonably use the application comfortably and it actually works, you're good. Course an issue may arise if a customer also tries to ask for extra elements that aren't exposed in the middle of development, but every paradigm carries that problem to some extent and you are trading dev time for scalability there. Everything is about tradeoffs too.
@@Spartan322 But again - look up how dangerous it can be for example when it interplays with threading via the example I gave - literally could kill people just as well as a badly placed goto. And you cannot leak out the this pointer in parent constructor if there is no parent and no inheritance (of implementation - maybe only interface) in the first place - which is just an example. Also honestly... GOTO is also only developmental quality because you can totally make good software with it too. As I said its not the GOTO itself that makes the issues - but sphagettified code that developers cannot see bugs of...
@@u9vata Threading is inherently dangerous regardless of what you use, because there is no determinism with threading, you can never expect any order or organization to what you use so it does not matter what paradigm you use, you cannot guarantee an outcome's correctness (in terms of flow or sequence) without extraneous and tedious work, and it is near impossible to track every hole you open unless you figure out how to convert literally every operation into an atomic operation. (which is generally not possible even in the simplest of cases, and even then its not performant and could even cost you more then any other paradigm you could've used) And that's before we consider race conditions, or if you try to prevent race conditions, you might cause a deadlock instead and now your program might be wasting time doing nothing or wasting a thread it can't access, or even just wasting resources keeping a thread alive for no reason. If anything threading is almost (and I literally mean) as deadly as goto, its only because its danger is so well seen that its unlikely that anyone is to step upon that lethality unlike goto, though even then I wouldn't call that rule, all it takes is one person to slip up once with threading, something even easier to do then with goto especially since debugging them is 10 times harder then goto ever was. It really isn't fair to bring threading into this argument when threading itself is the issue there, more then anything else you could reference, it can make things slower if used wrong which is easy to do, its not easy to intuit even in a simple case, it can break in any number of ways that allow a programatic valid state that should be invalid, it can deadlock the application and break expected sequence of the application in a hard to debug manner, which it is already designed explicitly to do.
I find it very interesting how classes in C++ basically work like they used to with babel before JavaScript had its native classes, where classes were transpiled to an implemetation used prototypes and a WeakMap.
What happens if you declare the function arguments and also the operations themselves as const, which they should be, what happens then when you compile with optimizations ? Could you compare the assembly of both ? C++ might surprise you here ;)
While you're right that adding `const` in general has the possibility of improving compiled code, in this contrived example there's no benefit to be had. There's no point in passing the arguments as `const`, since they're passed by value. Declaring the functions themselves as `const` also wouldn't help in this example, where OP is complaining about the indirect virtual call, rather than the compiler's selection of which function to use (which is what `const` helps with). On a more abstract level, it's also (often) a mistake to declare a virtual base function as `const` unless you're trying to establish a guarantee. A derived implementation (which would also have to be declared `const` to override the base class) might want/need to modify its members for some reason. Sure, it could declare those members as `mutable`, but that has its own philosophical problems.
There is a bit of important context for polymorphism as why it is used and what is it's bad at. Yes, hand coding C for simple operations might be faster, but when you're trying to make a large OO project, doing all those manual things in C can take a long time so it's only worth doing if code performance is very important for the project. In many cases for desktop or web based applications, the extra operations needed for C++ polymorphism are still magnitudes faster than waiting on data transfer betten storage or Internet data. It ends up not really mattering at all for the usability of the program.
This compare apples to oranges, `virtual` should be used when you do not know final types (like you get them form different TU or even DLL). If you know all types upfront you should use STATIC polymorphism in C++. C in your example have privilege to know all operations and C++ is deliberately hamstring by hiding all contrite types. Most funny if you try `qsort` in C you need use effective "virtual" calls where C++ `std::sort` use direct calls that compiler can easy optimize.
The external polymorphism pattern is what you really want there, and if you don't need runtime polymorphism you just make it a template parameter. I wish I could post links here without the comment disappearing :(
IM GOING LIVE TODAY AT 12PM EST TO REVIEW MY CHATS CODE: ua-cam.com/users/live-A6Ar4u_Teg
I think you should add that *runtime* polymorphism was discussed in the video.
C++ can also do compile time polymorphism (which doesn't have a runtime performance overhead).
@@kuhluhOG but it has other trade-offs
@@EmilPrivate mostly longer compile times, yes
@@michaelxz1305 It's relative
What's this text editor you used?
"It is important to note that by default, functions with the same signature in a derived class are virtual in the parent by default." What? No? This isn't Java.
I was just looking for this comment. Maybe he's not as experienced in C++ as he is in C.
@@reromanSure, but it's such an odd mistake. It was such a detailed explanation, but it was just wrong. Especially for a whole video built around why polymorphism supposedly sucks.
Is this perhaps c++ version specific rather than outright wrong? (not a cpp dev just a thought)
@@michaelgreene6441Think about what you're suggesting. C++ is old. If there were such a drastic difference in behavior between C++ versions, that would actually be terrible. This is simply wrong.
@@Spirrwell It would literally mean you can't move a project further forward on C++ versions, which does happen occasionally, if you try to use auto in C++98 and and then move to C++11 it will throw an error where it was once valid, but such things are rare and not common use, auto was functionally worthless by 1995 and almost no one used the feature and it was deprecated for over a decade, and almost no other elements of the language were reworked like that, other case I can think of is the comma operator in square brackets, which again was a feature nobody used because the comma operator is almost never used for its return value. (and otherwise the implementation for a[1, 2, 3] wouldn't be how you could access a 3 dimensional container which they deemed a nightmare to do)
I have a number of issues regarding this video:
First, the example given is a exceptionally poor choice. The number of unique cases is far to few for when Polymorphism should properly be used. With only "OP_ADD" and "OP_MUL" as outcomes, the code will compile into conditional jump statements. If there were more cases, the switch statement would likely be compiled into a jump table. This would have been a far better comparison to Polymorphism. As it stands, the video was effectively comparing if-statements to function pointers for a minuscule number of cases.
Second, This explanation on how vtables are structured is based on the assumption that the vtable isn't embedded into the object. While I haven't encountered any compilers that do this, actual implementation of an object's vtable isn't standardized. This video can wrongly give off the assumption that it is standardized.
Thirdly, in regards to why virtual methods are slow, while it is true that memory operations are slower than operations that take place on registers, the biggest slowdown actually comes from the fact your doing a dynamic jump. Modern CPUs try to fill the execution pipeline with as many instructions as possible with out-of-order execution by looking ahead for independent chains of instructions. This is far easier to do when the control flow of the instructions is static and works especially well when the branch predictor chooses the correct execution path. When a dynamic jump occurs, the processor basically has to halt everything until the memory location of where it's going is loaded into the processor.
Forth, I must disapprove of the blanket "This is Bad" approach this video takes. Polymorphism is just like any other tool in programming: there situations where it is good and bad. When choosing what mechanism to use in programming, one needs to compare the benefits and cost. The video you've done shows Polymorphism being used in an inner-most loop. This is quite likely the worst case scenario for polymorphism. It simply isn't worth the overhead that comes with polymorphism. If you wanted to stick with the calculator theme for the video, better options like Square Root, Trigonometric Operations, or Logarithm would have been a fairer comparison.
Lastly, the code presented at the start has some problems. Why are you using atoi in the conditional?! The code will be pointlessly executed every pass! Additionally, if you use optimization settings, the compiler might very well optimize away the entire body of the loop! If it tries to inline the operation code, it might very well see that the only one case is ever true and the operand assignments are pointless. This would result in an addition to a temporary variable that is only written to and never read from. Seeing it has no side effects, it might just empty the entire body of the loop. Unless you looked at the assembly, you wouldn't be able to tell how aggressively it optimized the code and if the tests were actually fair.
The video gives a clickbait feeling by bashing on something widely used so hard, that everyone using it would feel offended.
I also would like to add that the video invites to premature optimisation. 99.99% of developers will have bigger fish to fry in their codebase than two memory accesses
You left off
Sixthly, he incorrectly states that you can't do OO programming in C. It requires more discipline, and you don't have the benefit of any sort of language features, but there is literally a book called "oo in c". Unless the book is several hundred blank pages, i haven't actually checked.
Seventhly, sort of the the reverse of the above. If you really dying for that minuscule performance bump, you can write the code in C++ the same way you would in C. Without any late binding.
@@khatdubellMinus the restrict keyword in C, but I have never needed to use restrict.
@@MrJake-bb8bs I don't feel offended. Am I a freak ?
Cool video but i could'nt stop staring at the cpu with the swick sunglasses
good ole carl
Exactly... Me too
A non-virtual call is exceptionally fast, as it usually consists of a single instruction. On the other hand, a virtual call introduces an extra level of indirection, leading to the purported 20% increase in execution time. However, this increase is merely "pure overhead" that becomes apparent when comparing calls of two parameterless functions. As soon as parameters, especially those requiring copying, come into play, the difference between the two overheads diminishes. For instance, passing a string by value to both a virtual and a non-virtual function would make it challenging to discern this gap accurately.
It's essential to note that the increased expense is primarily confined to the call instructions themselves, not the functions they invoke. The overhead incurred by function calls constitutes a minor proportion of the overall execution time of the function. As the size of the function grows, the percentage representing the call overhead becomes even smaller.
Suppose a virtual function call is 25% more costly than a regular function call. In this case, the additional expense pertains only to the call itself, not the execution of the function. It's essential to emphasize this point. Usually, the expense of the function call is much smaller compared to the overall cost of executing the function. However, it is crucial to be cautious because though it may not always be significant, if you excessively use polymorphism, extending it to even the simplest functions, the extra overhead can accumulate rapidly.
In C++, and in programming in general, whenever there's a price, there's a gain, and whenever there's a gain, there's a price. It's that simple.
First your whole line of argumentation about the copy overhead is ridiculous. No one will ever pass something big by value unless it is absolutely necessary, and in that case the copy should be considered part of the actual work the function has to do.
If the function does a lot of work and is not called often then you eventually run into icache issues wich increases the overhead. If the function is called frequently the overhead adds up, doesn't matter that its small compared to the actual work that the function does, small multiplied by a big N is big, and if you are in a performance constrained system like a videogame renderer even small overhead should be considered. Another way in wich vtables are horrible for performance that this video doesn't mention is the fact that they introduce bloat into your data types wich is absolutely horrendous for cache efficiency, so even if you don't care at all about the indirect call overhead vtables may be a no starter to begin with.
Also what "gain" do you get from virtual functions? I would argue you get negative benefits, codebases that uses this kind of thing extensively do a lot of inheritance wich is in itself bad for performance (bad for cache since you end up with HUGE data types) and just overall terrible for readability and simplicity.
And even if you do single layer inheritance and you are very careful not to bloat your data types virtual functions greatly obfuscate the control flow of the program, because we all know foo.bar() calls the bar method in the foo object oh wait, foo has 5 subclasses and it could be calling any of those, so poor me running my program through a debugger will be met with a suprise when I hit that line.
Readable and pretty are different things, the way you do it in C is not the prettiest but it is the more readable, in a switch statement you can clearly see how many "overrides" there are and what needs to be true in order for every function to be called. You can step through with a debugger and see everything that is happening and not have to worry about vtable nonsense that may be happening in your back at any moment.
Never ever use virtual functions, they are always bad, and I absolutely mean this in absolute terms.
@@marcossidoruk8033 I mostly agree with you, but virtual-functions/dynamic-dispatch still have their place.
A simple example would be a video game, where the Enemy type can be anything and should be easily extensible without changing the calling code.
About the object bloat, there's an alternative you may have seen in Rust - dynamic types. The vtable only exists if the type is marked as dynamic, which means the original type doesn't have any bloat, but references to it become fat pointers (pointer to object + vtable).
@@marcossidoruk8033 To be fair when you have a switch statement, all possible types are known at compile time. When you use vtable you can, for example, add types from other dlls and it'll still work. These are different functionalities
Also I feel like languages should introduce such closed polymorphism where virtual functions would compile to a function with a switch inside, would be kinda cool
@@marcossidoruk8033 I never said any of this
@@marcossidoruk8033 The second you start running code where data loaded from storage influences the order of execution (very common in a game engine), the control-flow argument goes out the door. Yes you can still mitigate some of the chaos away by processing the execution graph into a more structured form - but some will remain anyway it will never be completely predictable. A much worse offender of control flow obfuscation than vtables are collections of asynchronous lambdas (jobs) processed by a multi-threaded worker queue - that makes me pine for the simple days of a running a method on a collection of base classes with vtables.
Remember to mark your polymorphic classes as final if they are the final derivation! That lets your compiler optimize virtual function calls into regular function calls in situations where there can be no higher superclass.
this is c++ not java, unlike what low level said, C++ classe are actually "final" (aka not virtual) by default
I get your point: virtual function have a cost and it's true.
But the video is not 100% honest. The c++ code can be extended by only creating new type of operator without touching the code that execute operation. The C code cannot, you must change the central switch case and the enum. The virtual function has a cost but also offer functionality. Do the functionality worth the cost? Maybe yes maybe no. Each case is different and must be evaluated.
Also c++ != virtual function and inheritance. It's not because the feature exist you must use it. They are tools in the toolset, nothing more.
Then you also did an useless operation the force the c++ to pass by the vtable with the line "Operation *op = &add;". If you just used "add.execute();" directly the compiler you know at compile time what function to call and would not pass by the vtable. I understand you did that to have a one page example. But it could lead someone to think an example this simple would always use overkill feature like the vtable. It make look c++ dumper than it is.
I agree. In fact, it's probably zero cost if you use templates (at least in Rust). In this case, he is using a pointer (in Rust, it would be `&dyn Operation`, the dyn makes it clear it's a fat pointer)
you only have to write it once but the user will have to pay the runtime cost forever !
@@nero008You do know that the vast majority of useful code needs maintenance well into the future, right? Not to mention that this sort of thinking almost always falls under the umbrella of premature optimization. Outside of very select circumstances, the performance difference between a polymorphic implementation and a monomorphic implementation is utterly imperceptible to the end user, but the difference it makes to the structure of the code could be very well noticed and appreciated by any programmer doing work on that code
@@theEndermanMGS why the hell would i base my point on cold paths ?
@@nero008 You certainly didn’t read that way, given that you were responding to a critique of the video’s lack of nuance that makes clear that there is room for evaluating whether a virtual call’s performance penalty is worthwhile in a given case. I don’t see any reasonable way to read your response except as a complete dismissal of the more nuanced view.
I've been coding a game engine from scratch for a few years now, and in most real scenarios I encounter, where functions perform actual work, the virtual call overhead is just negligible. I tend to avoid using virtuals in a hot path (functions that will run many times per frame), but I'm totally fine using them elsewhere.
This is how I dispatch game system update and render calls in my framegraph, for instance. My game systems must have state, and the engine can't know about the specifics of client-side game system implementations. All it knows is that some will modify a scene (update), and some will push render commands in a queue when traversing a const scene (render). So polymorphism is a good tool here: it makes the API clear enough, it makes development less nightmarish, the abstraction it introduces can be reasoned about, and the call overhead is jack shit when compared to execution times.
Guys, don't let people decide for you what "sucks" and what is "nice" or "clean". This is ideology, not engineering. Toy examples like this are *not* real code, measure your actual performances and decide for yourself.
Yep, you can believe in whatever programming ideologies you want, but it all goes out the window when you are in a real world situation and actually need to get something working.
I can't find a source on this, but I remember reading around a decade ago, when I started uni, that the OGRE engine was made the way it is was partially to demonstrate that the overhead of virtual calls is neglible.
@@jaskij Nice, I didn't know about this! Can't find a source either, sadly.
If you can ever find it back, I'd be thrilled to read about it @@jaskij !
You deserve way more upvotes, the example in the video isn't a typical scenario where you'd want to use polymorphism at all.
In the famous words of Chef "There's a time and place for everything, children. It's called college"
So true 😂
Or enterprise grade software 😂
$70K for something you can otherwise learn online by yourself? Nah. Plus modern college is predominantly online anyway. Professors don't have time to answer intricate questions and tell you to google things anyway. Absolutely pointless.
@@user-og6hl6lv7p that’s quite the generalization. All universities and differing degree programs are different.
@@user-og6hl6lv7p that's not the case in The Netherlands, don't generalize your experience with other countries.
It's always worth noting that when Stroustrup says that C++ obeys the zero-overhead principle, that in NO way means that the abstraction itself is free. It's just that you couldn't hand-code it better yourself with less performance overhead to use that feature (ideally). If you use inheritance with virtual member function overrides, you *will* pay a cost, because if you care about performance 1. you should always measure it, and 2. you should be aware of exactly how it is implemented. Otherwise, don't solve the problem that way.
There are some cases where inheritance is quite applicable, but needless to say, it is not exactly cheap, and its depth should be minimized if it's used at all. And to quote Gang of Four, "Favor object composition over class inheritance".
I'm a newbie to OOP and I hear "composition" quite often but I don't get what it means. Can you please explain it in a few phrases?
@@heavymetalmixer91 it's as simple as including one or more instances of a class inside another class rather than deriving from a base class. So a class that contains objects of another class as opposed to deriving from another class. This is easier to understand especially compared to multiple levels of derived classes
Starsoup is full of shit and according to your definition "zero overhead" is actually not zero overhead.
This is like killing some people and then saying "I killed zero people according to the zero kill principle because you couldn't have done any better" its nonsense.
Zero overhead means zero overhead, what else would it mean? That is the abstraction costs no extra cpu cycles and that is exactly what starsoup means, he just never claimed vtables to be an example of what he calls a zero overhead abstraction.
However, the whole idea of zero overhead abstractions is in itself ridiculous, no abstraction ever has zero overhead, and if it does it is probably a terrible abstraction that doesn't provide you with much abstraction to begin with.
@@heavymetalmixer91 Instead of extending the base class you put the object of that class inside of another class. That way you still have access to all of the fields of the "base" class but usually you need to write some additional code like setters and getters and method wrappers in the new class to get to those fields and methods. In case of c++ you can achieve exactly the same result by inheriting the base class without using the virtual keyword. The fields of base class are just going to be included in the structure of derived class and methods will be statically resolved at compile time (no vtable and no double dispatch). But it doesn't work that way in other languages. For example in java every method is virtual by default.
@@heavymetalmixer91 As a practical example, say you have a Bow and a Sword class, and you want to make a SwordBow class that is like a Sword and also shoots an explosive arrow out of the sword. You could do this by letting SwordBow inherit Bow and Sword, and then overriding the inherited Bow's shoot() method (function) to replace it with a new method that shoots an explosive one. With composition on the other hand, you simply tell Sword and SwordBow to both use a slash() method, and you then define the explosive shoot() method in SwordBow without overriding anything. Composition turns a 2D family tree into a simple 1D list of family members.
Mistake in 03:46: If a derived class has the same function as the parent BUT the parent function isn't marked as virtual it doesn't become virtual implicitly. The 2 functions will co-exist simultaneously. Which one will be called depends on the type of the variable that does the call. Is it base class variable or a derived class variable? Specifically if the pointer is of type base*, you assign it to an instance of derived class and call the overriden function it will actually call the base class's implementation of that function. This is a subtle C++ pitfall that can lead to bugs.
What you probably meant to say: If a base class has marked a function as virtual then it becomes implicitly virtual for any derived class. The derived class doesn't have to explicitly mark it as virtual. But for clarity reasons it should do so.
The confusion likely stems from the default behavior in Java, where all methods in derived classes are virtual. This design makes sense for a garbage-collected language, as it can directly optimize pointer indirection. In contrast, Go takes an interesting approach: instead of storing the vtable pointer in the derived class, Go places it in the base class (referred to as an interface). In Go, each interface instance is 16 bytes in size, consisting of two pointers: one pointing to the vtable of the derived class and the other pointing to the actual instance of the derived class.
In C++, however, the behavior differs significantly:
1. Virtual Methods :- When a method is declared as virtual, it is invoked based on the object's runtime type, regardless of whether the object is allocated on the heap or the stack. Each object stores a pointer to its vtable, which is used to resolve the method call. If the object is heap-allocated, there are two levels of indirection (heap allocation and vtable lookup). For stack-allocated objects, there’s only one level of indirection via the vtable pointer. While the compiler may optimize heap allocations into stack allocations, virtual method calls always involve pointer indirection.
2. Non-Virtual Methods :- For non-virtual methods, the method call is resolved at compile-time based on the class type of the object. If the method accesses non-static members, they are accessed through the "this" pointer. However, since the compiler knows the class type at compile time, it can often inline these method calls at the call site. The choice of which method to invoke is based on the class where the method is declared, not the runtime type of the object.
80% to 90% of code execution time is spend in 10% to 20% of the code. if your code is performance tuned so much that it's the performance degradation is due to vtable lookup (which I highly doubt), then perhaps you can say "polymorphism sucks". For 99.999% of all programs out there, that's not the case and polymorphism is a very useful feature and makes code simpler.
Yeah this is an extreme example because in this case, the polymorphic function in question is just: int add(int a, int b) {return a+b;}
And yeah, if you're doing a bunch of one line functions it'll probably make your polymorphism overhead seem really bad. Polymorphism isn't designed to optimize toy programs, it's designed for large projects.
You say 99.999%, and your average programmer unhappily employed doing webdev might be able to agree.
The problem arises when these techniques and philosophies seep into applications programming (or worse, embedded) where the bottleneck is no longer some disgustingly lethargic network activity.
Then your 99.999% becomes merely "sometimes." Are 99.999% of all web-free programs I/O bound? No, not remotely. And for any of them that aren't bound by even disk I/O (nevermind the abyss of network I/O), a benchmark between polymorphic and non-polymorphic solutions will show a non-trivial difference quite a bit more frequently than you're claiming.
That small bit of code you mention that occupies the most runtime? That might be a tight logic loop or some recursive tree/graph-walking thing, which isn't that unusual for a program that's algorithmically interesting. And yeah, you'll absolutely see a difference if that logic is running through some enterprise-grade polymorphic mess.
So it's truly unhealthy that OOP and web/enterprise laziness, which is excusable in that one specific realm, has infiltrated universities to become embedded in the minds of all graduates, when only *some* of those graduates are going to be employed writing code that's already so slow that polymorphism is just a drop in the bucket.
(And yes, I do understand your point applies to these performance-relevant programs wherein the bulk of the computation time lies within the bodies of functions rather than straddling calls that may or may not be subject to a vtable lookup, but if you want to make that argument more effectively, you'd need to shift that rectally-extracted statistic from 99.999% to something more like 80% to avoid being too obviously hyperbolic)
@@delphicdescant True. Recently was doing some benchmarking on array performance in C# and... let's say that [y*width+x]!=[x,y]!=[x][y]. And none of those is the same as objects encapsulating them. And none of the former is the same as accessing said objects through an interface.
That said, such is the cost for "generic not specific". But... if you're laying tracks for a trolley car, you have a LOT more tolerance than if you're laying tracks for a bullet train. People just need to be taught "when is which". And I bet most will want to stick to either side of the fence in their work. "Application coders" and "performance coders" are intrinsically different, but both have their place because business needs vary.
@@taragnor its designed for projects that dont require hyper performance, if your project is big but needs high performance then you will start needing to not use these abstractions depending on how much optimization you need and where the bottleneck is.
"polymorphism is a very useful feature and makes code simpler" - do you have an example of code made simpler by polymorphism, or are you just repeating the marketing ads of OOP people?
This video shows a only a compaison when C++ virtual approach is disadvanaged and dosen't count the disadvantages of the C switch approach, using that to arrive at a simplistic conlcusion "virtual bad".
The example show in this video the number of operation and the operation itself are very small, so here the comparison is "dereferencing a virtual table and call a function" vs "call diretly a slightly bigger function". But if we pass in a more realistic context, where there are more derived classes and bigger method, now in the C approach we have bigger switch that've to call other functions so that means that we've to do 2 function call in C approach insthead of 1 like the C++ approach, with all the overhead that comes to (or put all the logic of all cases in one function and have a massive switch). And THE CACHE MISS CAN HAPPEN EVEN IN C APPORACH TOO, you know the switch has to be loaded form memory too, like a vtable.
The C switch has the advantage of being easier to optimize and can be faster, but you've to now and implement the call for every type of operation in the base class. This means that it's not possible to use in situations like extending the functionality of an already compiled library.
And last which compiler and flags were used?
You can write OO code in C. C just doesn't do the heavy lifting for you. You can write your own dispatch tables with function pointers.
There are plenty of OO libraries for C. GTK+ and Xt are two examples.
The code doesn't look as nice, but OO is about how you organize and reason about your code, not about whatever syntactic sugar your language gives you.
A feature of the language is not just 'syntactic sugar', because if it were, then C is just 'syntactic sugar' for Assembly.
While all of this is true, I think the main point of the video was that type erasure is just not a good idea in general for a staticly typed language from an efficiency pov, so c++ making it easier is a debatable choice(at least in nowadays perspective, I'm not suggesting that Bjarne should have looked at his crystal ball to see that in the future the additional level of indirection would mess with the branch predictor or the icache)
Right, OOP is a paradigm, not a language feature. C++ used to originally just compile into C first before it got its own compiler. You can do almost anything in C, it just may take a godawful amount of boilerplate that other languages will handle for you with ease.
@@GregMoressNo, in fact C doesn't even target your CPU (thats what your compiler does), C targets an abstract machine.
You are bluring the line I drew, so allow me to blur yours...
In Java and DotNet, and even Basic, there is/are intermediate OP Codes, MSIL for DotNet, ByteCode for Java, and P-Code for BASIC.
It's possible for C to have such 'intermediate' Op codes as well, which is the Assembly language that programmers would write to if not for hi-level 'syntactic-sugar' languages...
The fact that C doesn't use OP codes doesn't change the fact that a programmer COULD write Assembly targetting their CPU. An internally, the C compiler DOES have platform-agnostic OP codes.
The difference between C and C++ is not just 'syntactic sugar' it is a feature, just as C is a feature, and not just 'syntactic sugar' over OP codes.
I teach college C/C++ and I think the most important part of introducing C++ is explaining the problems that the language was written to solve; they were problems with large scale software development, not writing faster algorithms. Projects were becoming really hard to manage with library name conflicts, simultaneous changes to common code stepping on eachothers toes, human problems like that.
Polymorphism lets developers build on top of eachother's code in a way imperative code can't support. Someone could subclass your calculator, add/remove/change operators, and pass the new calculator into existing calculator-using code with no change to your calculator class code and just instantiating a different class in code that depends on your calculator.
Indeed, it's a common theme with these UA-camrs that they take very simple coding problems and think they can extend those experiences to all coding problems, but when you are not alone working on your own codebase and instead of a team of half a dozen, or god forbid dozens of people and you don't implement polymorphism you are going to grind progress to a halt needing to rewrite a bunch of code and any new members to the team need to first spent weeks or months studying the entire codebase...
Bravo! 👍👏👏👏 Exactly!
Polymorphism is all over the place in linux kernel (written in C). They just have to define vtables manually.
You made me think of C#. I appreciated the interface there, and realized quality GUI frameworks require OOP. There are these use cases as important as selecting the right language for the job at hand. If you make a backend or low level dev look at some AI models, they will flip out. No, go ahead and do that functionally. I'll check up on you in ten years.
This video is misleading. Even if the vtable overhead affects the runtime of a time-sensitive program, this video misses the point of polymorphism to begin with. In most cases where dynamic dispatch is preferred over static dispatch (e.g., game scripting, networking, client programming, GUI apps, etc.), the overhead created by vtable accesses is negligible. You would lose more processing time in a program that statically generates similar functions for hundreds of classes and the various functions that take those objects as parameters than if you were to use polymorphism for said objects. There are different applications for both static and dynamic dispatch, and it is misleading to hold them to the same expectation.
In large project, polymorphism could literally save tons of amount of time and be really helpful to organize the code. Things like OOP, design patterns, are basically just for large projects but not for something small like calculator or some kinda stuff.
Exactly, they aren't done to speed up execution time, they are done to speed up development time
Thing is, they don't always make your code easier to write. Using polymorphism often comes from the assumption that your code has to be generic on order to be future proof and easily scalable. But the reality is: very rarely do you actually know whether you will need this extra scalability in the future, ESPECIALLY in large projects. Using stuff like enums, or sometimes type erasure doesn't have to be that complicated (I mean enums are really simple, seriously) and they can minimize the performance overhead. Polymorphism can be okay, if you KNOW that you need it for any given task. If you're just trying to plan ahead chances are you will mess it up because no one is that good. Maybe it's okay when you only have a few polymorphic classes with a single inheritance level... but when your WHOLE API is almost exclusively polymorphic, with sometimes 3 or 4 levels, it's a nightmare performance wise, for a minimal benefit
@@eddiebreeg3885 Agreed, if you try to use polymorphism on everything you are going to have a bad time. I look at it more as a tool for very specific things. Any feature of a language can be bad if used without care.
The thing with OOP is it's far more flexible. Imagine you wanted to add a new operation. Using the C method, you'd need to be able to directly alter the original source code. If you're using an external library, that may not always be an option. Under C++ polymorphism you can build a library that can be extended with new operations simply by extending the base class and you never need to alter the base code. And that's one of the main advantages of OOP, being able to create libraries of classes and functions that people can just drop into their programs and use.
(or you could just use FP and have nice abstractions from the start)
I'd say that polymorphism doesn't necessarily suck. Poor use of polymorphism sucks. If your virtual function takes only couple of cycles to complete, then it might not be a good idea to use polymorphism, but when it takes couple of miliseconds, the slowdown caused by the extra lookup is negligible.
Nailed it.
You explained very nicely why polymorphic code is slower, but it sucks only if you need to squeeze all possible performance from the hardware by a specific task. Which is the case pretty rarely. The most common scenario is something (I mean, most of the code) is waiting for something slower, meaning it has more than enough time to not create any measurable lag. But yes, when you're optimizing a tight loop and every cycle matters, then the good old C is the way to go. But again it would be pretty tricky to provide a right example. In my practice - if I have a tight loop and I want to save every cycle - I just avoid calling anything, inlining as much as possible. And this can happen inside a C++ overridden method, but it just doesn't matter as long I don't call other other methods from said tight loop. Sometime I mix C with C++, where C++ is for general app logic, and C is doing time critical things. The common C++ usage is for tasks too tedious to code in C and usually not time critical. Like UI and network. The code waits "ages" for something to happen, then you invoke a C function that does the heavy lifting pushing gigabytes of data because the user just moved a mouse or clicked a button.
Yeah the title should have been specific for C++, instead he chose to clickbait and generalize it to all code, which is not the case.
If you need to develop software with a team of people not using polymorphism will cause more problems than the potential performance loss, which in most real world cases that aren't basic C++ scripts running on Arduino don't exist in the first place.
This is a nice video but I think it's also a bit misleading...
The C code isn't *really* polymorphic: Every 'method' added has its own switch statement, every 'child' added is a new case for every switch in every function and the monolith grows.
And the C++, of course it'd be silly to use that kind of indirection on the hot path (and std::visit(); might be preferable).
Either way, the compiler will optimize away where possible for the target platform, including switch statements.
Polymorphism doesn't need to involve dynamic dispatch though. In Rust, for instance, polymorphism can be achieved with generics. And the cool thing about generics is that at compiler time they get monomorphized, i.e. the compiler will just copy-paste the function with every combination of concrete types that it's called with, so you just end up with roughly the same setup you had in C with no performance cost.
I haven't had much experience in C++, so I don't know how templates work there, but presumably it would be possible to do the same thing there.
Also there are situations where dynamic dispatch is needed, so, if you're writing it in C you'd use function pointers and it would be just as slow as virtual methods.
Incredibly misleading. You're not comparing the same things. Full-fledged polymorphism with virtual calls cannot be compared to a case over an enum. If you were comparing C++ to a function pointer call (with the destination of the pointer compiled in a separate TU and the program compiled without link-time optimisation) in C, that could be considered "comparable" at least.
Sounds deceptive to say it sucks. It doesn't. However, it's easy to misuse.
some might say that tools that are easy to misuse categorically suck more than tools that aren't easy to use at all, though that wasn't his point, so that wouldn't help his case.
I mean this guy just doesn't know what he's talking about, which is rather apparent. This whole video carries the same argument as "random access data structures suck! they're slow!" when all he's doing is performing tons of insertions and deletions at the middle of the list. He takes 1 example and compares it to another completely abused case of a language feature and draws the conclusion that it "sucks".
Not many people can make an 8min long video about polymorphism and such low-level details and still keep it interesting. Well done, bro.
Wow, thanks!
Just unfortunate that he was wrong regarding pretty much everything.
@@valizeth4073 I found the video very informative and engaging. Can you specifically address where he went wrong?
@@justaboy680 E.g., the claim that because C is not an OO language, you cannot write OO code in it. Of course you can. What do you think fopen/fread/fclose do, if not return and work with an object handle with virtual member functions?
@@ccreutzig I'm not aware of the implementation details of file utilities. What virtual member functions are you talking about?
C can be written in OO style (see GObject), it just requires you to manually implement the pattern. An interface in C would be a structure containing function pointers constructed by object specific functions.
For such simple operations vtable lookup represents 20% more of execution time but truth is in a real program it would not represent such a high proportion of execution time. And C code might be less easy to maintain down the line
Yeah most functions aren't going to be "return a + b;"
I LOVE polymorphism. Let me tell you why. I've used it in providing an undo/redo feature, as well as in writing an arithmetic parser. It elegantly separates the overall design from the implementation of nodes. So if I have to fix nodal behavior or add new node types over time, I don't have to touch the top level architecture. It. Just. Works. Yes, there is a small performance hit but it's more than worth it to me to have more readable, more maintainable code. That said, one alternative is to use name overloading (what I like to call "static polymorphism"), which is resolved at compile time and solves the performance hit.
You don't need to recompile the binary containing the base class when you need to add another child class. You can swap functionality on linking stage. You can plug in different implementations without recompiling the core logic. That's Open-Closed principle for you - the parent class stays closed for modifications, it literally never gets recompiled anymore, but open for extensions - you can add child classes freely and compile them separately.
I used polymorphism in writing simulations where the code paths were things like "run this CFD code that takes 20 min" vs "run this approximation that still takes 30 seconds". It made for convenient organization where the child classes would track whatever extra data they needed. The overhead in using virtual functions maybe added a few millionths of a percent to the overall runtime of that particular feature.
About the V-Table, if you use the final specifier on virtual functions this(amongst other things) provides the compiler an opportunity to "devirtualize" the function.
Well, this has been discussed dozens of times already. Essentially the example you constructed is specially crafted to make polymorphism look bad.
If you want to do a simple calculator, the operation can be done in a single instruction and fits in register.
For the types of code that OOP was designed, though, it has to search data structures which use memory allocated in the heap, fetch data from storage, invoke kernel syscalls, etc, and all of this will make the overhead of calling a vtable negligible.
And even in the case that you find yourself in a situation where you need to have the CPU call an operation millions of times you could just dump the class structure with other code, translate it to C style opcodes and run this instead.
This is what any numerical library in a virtualized language does, btw. And if you do your calculation in the methods it provides, instead of using the language directly, it's just as fast as C code.
At the end of the day, the only performance penalty you cannot get away if when using higher level abstractions is the start up time it takes to initialize all the stuff, and a fixed amount of memory to store everything.
I definitely think yoh could provide better content to your viewers than something which already has been exhaustively discussed in the 80's.
Firstly, I'd like to emphasize that polymorphism, like any tool in the programmer's toolbox, has its optimal use cases. It's not inherently 'bad' or 'good', but should be used when appropriate for the problem at hand. An overly narrow focus on performance can inadvertently lead to premature optimization, and it's important to remember the classic Donald Knuth quote: "Premature optimization is the root of all evil".
Polymorphism shines in scenarios where you need to process a collection of objects that share a common interface but have different internal behaviors. In these cases, it allows you to write more concise, readable, and maintainable code by avoiding verbose switch-case statements or if-else ladders. Although polymorphism introduces a performance overhead due to the indirection through the vtable, this is often insignificant in comparison to the cost of the actual function execution. And while it's true that in a tight loop this overhead can accumulate, this isn't where polymorphism typically brings the most value.
To your point about how vtables are implemented, it's true that there's no standardization across compilers. However, most C++ compilers adopt similar strategies, and the vtable is usually stored separately from the object instance, with a pointer to the vtable added to the object's memory layout. This additional level of indirection can indeed add to the function call cost, and that cost is further compounded by the challenge for CPUs to predict dynamic jumps, as another commenter rightly pointed out.
I also agree with the critique about the video's simplistic example. A more complex set of operations would have made the comparison more meaningful, as the benefits of polymorphism become more apparent when you're dealing with larger codebases and more complex behavior differences between classes.
Finally, it's worth noting that C++ offers more than one way to achieve polymorphism. Beyond the classical (runtime) polymorphism we're discussing here, there's also compile-time polymorphism, also known as static polymorphism, via templates. The latter can eliminate the runtime overhead, at the expense of potentially increasing the binary size. It's yet another trade-off to consider based on the specific needs of your program.
Overall, I think it's crucial not to dismiss polymorphism out of hand because of performance concerns. Instead, we should understand its costs and benefits, and use it where it makes the most sense.
"Firstly, I'd like to emphasize that polymorphism, like any tool in the programmer's toolbox, has its optimal use cases" - name one.
@@T0m1s
Well if you had read a bit further than just the first few lines you'd see that I'm providing actual examples, starting at "Polymorphism shines in scenarios...". I will forgive you though since attention span is a common issue these days. Below is another example that will further elaborate this.
Suppose we have a number of distinct shape classes, such as Circle, Square, and Triangle. Each of these shapes can be drawn and resized, but the specifics of how these operations are implemented vary between each shape type. By defining a base Shape class with virtual 'draw' and 'resize' methods, we can implement polymorphism.
Through this approach, we're able to maintain a collection of Shape pointers, without needing to know the specific type of shape they point to. When we need to draw or resize a shape, we simply call the appropriate method. The correct implementation is chosen at runtime based on the actual object type, thanks to the magic of polymorphism.
This not only makes our code more concise and maintainable (as we're not wrestling with unwieldy switch-case statements or if-else ladders), but also more extensible. If we decide to introduce a new shape type into our program, we simply create a new class that extends Shape and provides its own 'draw' and 'resize' implementations. No need to touch existing code.
Polymorphism thus allows us to write code that is open to extension but closed for modification, embodying a key principle in object-oriented design, known as the Open-Closed Principle.
@@T0m1sIterator objects. Also definining common interfaces for data structures.
For example you can have a Collection class that defines a search(x), insert(x) and remove(x). Then when you have an object with the type Collection you know you will be able to call those methods, ignoring the actual data structure used under the hood.
A pseudocode example could be:
class Company:
def getWorkers: Collection
then you can perform
c = Company(...)
...
w = Worker(...)
workers = c.getWorkers()
workers.search(w)
You can ignore how the Company class stores its workers (it could be a hash table, a double linked list, a Tree, etc..), all you care about its that it will return a Collection.
Nice video! There's a miss at 2:15: C++ classes are not C structures with function pointers. That's true only for polymorphic classes - and not even for all types of polymorphism, if we have to be picky. C++ classes are basically just structures, with member functions being normal functions with an implicit "this" argument. It's also worth mentioning the use of the "final" keyword can help assisting the compiler in optimising away virtual table lookups when virtual functions are called via the final implementation. Still, I prefer the C way as it doesn't add hidden data members and doesn't perform the shenanigans I described in this comment. Much easier to reason about.
Function with the same signature in a derived class is not virtual by default in a base class as stated somwhere around 3:40. You need virtual keyword in a base class to enable dynamic polymorphism. It's not only for code readability.
To be fair to all those C++ code bases littered with virtual functions and interfaces: With C++ not having a clean way to define restrictions (SFINAE my ..) for generic/template parameters until C++20 (and I have not actually seen concepts used in any productive code base to this day..), it was often the easiest way to get at least somewhat sane compiler messages, even though static dispatch or an enum style solution would have been perfectly possible for a lot of these usecases. Rust (btw) does it kinda cleverly by combining dynamic and static dispatch into Traits, although using trait objects is somewhat frowned upon at times.
In C++17 you can use std::variant and std::visit to do static dispatch with clean type like Rust enum.
C++20 concept is syntactic sugar for my point of view.
Almost everything can be written and read easily with constexpr and static_assert.
And SFINAE can be used to test the existence of an expression and transform it into a trait (a la std::is_arithmetic).
To be fair you can't really do generic programming in C ;)
I don't know why you are saying that trait object are frowned upon, lot and lot of widely used library abuse it, like tokio for exemple. you dive into any async library and you will see Pin everywhere (often aliased by BoxedFuture), it is just so much easier to work with.
C++ as a language, is capable of solving the problem introduced in the video faster than C contrary to the argument. If you need to have a “dynamic” operation (like a custom operation for a mapper or a predicate for a filter), on C you are stuck with function pointers (the given enum example is unrelated to the problem space IMO, it is fully static and not bound late at execution time) and they cannot be easily inlined by the compiler as they are only known very late at execution time. C++ OTOH, is capable of doing the inlining at compile time thanks to the superior type system. Overall I think this problem is a pretty bad example for polymorpism. It is just not the correct tool for this use case. Of course there are many ways of achieving the same thing and some of them will perform slower and it is something to consider when picking a solution. For the provided example, a compile-time polymorphism strategy (ie changing the operation at compile time) would perform the same as the C version provided. Note that the C version would not be versatile as you cannot simply provide the operation at the callsite and you’d need to modify every place you want to use that operation without a function pointer.
it cant really inline without LTO, and if you define it in the header, it can inline it, but same would apply to C, if you use a custom vtable
Fun fact: doing all of that stuff in C is completely feasible, altough it's unconvenient (lots of boilerplate code is necessary, C just is not object oriented) and unsafe (you need to do type checking at runtime). I actually discovered these things on my own a few years ago, when I first began studying OOP and tried to replicate it in C, which was the only language I was familiar with at that time. When you actually implement this stuff, you realize how much wasteful it is.
Btw, please note that there are many types of polymorphism. This is specifically what's known as inclusion polymorphism, which as far as I know is the only kind of polymorphism which has to be implemented with these type of runtime mechanisms. However, polymorphism is not something which is exclusive to OOP and stuff like adhoc polymorphism (ie function overloading or even traits in Rust) and parameter polymorphism (ie templates) are usually implemented statically. I know that there are a few other kinds of polymorphism, but I don't know much about those.
Good point, but in the calculator example, the C code has conditional code that the C++ code does not have.
Believe me, after having programmed in C++ for 30 years, I can assure you that the supposed overhead of polymorphism is negligible in the overall execution time of a real world program.
Depends on the specific case. If you loop over 100 million elements and call an add function (virtual or otherwise, as long as it’s not inlined) in every iteration then it’s way heavier than just summing the numbers in the loop. As always, it’s about choosing three right tool for the right job.
@@maxp3141 Ok, but this is why I specified “real world program”. I have no experience of real world programs that loop a million times calling a function that merely performs a sum. Functions are typically more elaborate than this, so I think that my reasoning is sound.
The basic point you're trying to illustrate about the vtable is correct, but I think we have to clarify a few things. First of all, this is not about C++ but about the code style your're using (runtime polymorphism). If you hadn't used a manual way of creating a function pointer, the compiler would most likely have resolved the polymorphism at compile time. Speaking about compile time, you could also use templates for enforcing compile-timepolymorphism. And modern CPUs are good at resolving indirections like vtables, too.
All in all, OOP and polymorphism can be great for structuring code, but I think they're overused.
It's not always about performance, you have to choose wisely what you want to use for your particular use case. If your use case requires raw performance, OOP may not be the way to go. But if you don't want to spend 5 hours debugging your C code, maybe OOP is the way to go.
Still, it's very nice to see someone explaining the implementation details of C++. Great work!
Why would a procedural code be a mess? It's usually really flat, as opposed to OO code. Which in my experience makes it a lot easier to debug, there's a lot less to worry about overall, functions are the only abstraction mechanism. Sure C++ compiler is better, which why people write C-style C++ over C.
Or you choose a language that doesn't suck. OH, WAIT!!!! There isn't any!
Until I finish and publish Nemesis that is!
@@filipg4 I didn't mean procedural code in general, but highly optimized code.
"if you don't want to spend 5 hours debugging your C code, maybe OOP is the way to go. " - OOP changes 5 into 50-500 hours, which is good for job stability
I only use virtual functions for my callback classes, it's nice to have syntax that wraps up functions in a single class and doesnt need to use C's terrible function pointer syntax. Never heard someone using it for "debugging" reasons
I've never needed to use inheritance in C++, I can usually get everything done with single level classes
I think that the concept of this video is wrong from the base. I mean: polymorphism sucks... if you need performance. But when what you need is code easily maintenable, and the performance is not a problem, it is a good tool (but don't forget what happens when you only have a hammer...). If performance were the only metric, we would use only assembler/C, and never languages like python, perl or java.
The c code in the video already looks so much uglier than the c++ version. Just imagine building huge libraries such as image adapters. It would look like spaghettis for sure in the C code.
lmao, we would absolutely NOT stop using java if performance was the only metric - we would stop with enterprisey patterns though
Meh. Try writting java on a memory constrained device or for real-time systems.
You can see how atoi is called in the loop to parse the second argument again and again ... I assume that's more operations than the virtual function
This is a mood point: sure if your method contains only one line then yeah, it will be slower but if you write code like that, you either aren't a good coder or you are writing java ;)
Hence why devirtualization is a big subject for compilers, making this problem disappear when possible. Using sealed classes at the end of the inheritance chain helps for devirtualization.
An alternative title: "How C++ implements polymorphism and its cost"
Enjoy your videos, it's great to see videos explaining the inner workings of the stuff that we just take for granted now.
I think you may have miss spoken a couple of times around 3:32. The "=0" is not needed in order for the compiler to create the v-table entry, this just indicates that the class is only to be derived from and that the derived class must implement this pure virtual function. Non-pure virtual functions (without the =0) need not be implemented in the derived class if the base implementation is appropriate.
Also you say that the virtual keyword is not needed and that by simply declaring a function with the same signature in the derived class the compiler will assume the base method to be virtual. I think this is true in some other OO languages, but not in C++. You cannot make a base class virtual without specifying the virtual keyword in the declaration of the base class.
It is true that you can declare a function with the same signature in the derived class - this is know as "overriding" (which is not the same as "overloading") - but it will not get an entry in the v-table and will therefore not get called by a method invocation from a base class object.
The following g++ program demonstrates this:
#include
class base
{
public:
void identify(void) {printf("%s
", __PRETTY_FUNCTION__);}
virtual void virt_identify(void) {printf("%s
", __PRETTY_FUNCTION__);}
};
class derived : public base
{
public:
void identify(void) {printf("%s
", __PRETTY_FUNCTION__);}
virtual void virt_identify(void) {printf("%s
", __PRETTY_FUNCTION__);}
};
int main(int argc, char *argv[])
{
derived d;
base *b = &d;
b->identify(); // calls base::identify() (not a virtual function)
b->virt_identify(); // call derived::virt_identify() (via vtbl)
d.identify(); // calls derived::identify() (overrides base::identify())
d.base::identify(); // calls base::identify() (explicitly)
b->base::identify(); // calls base::identify() (explicitly, bypasses the vtbl)
return 0;
}
Program output:
void base::identify()
virtual void derived::virt_identify()
void derived::identify()
void base::identify()
void base::identify()
OOP is just a tool. It will be great if you know how to use it.
PBRT use very much OOP a lot.
It really difficult when I tried to port their code to Vulkan and GLSL, but then I realized why they choose to use OOP.
OOP is the analysis. Using objects is just common sense.
The real problem is actually the loss of a lot of function inlining opportunities. Even with a switch table, the compiler would be aware of the complete set of behaviors and can inline the function calls. The situation is even better with compile time polymorphism like with CRTP, which often produces the most optimized binary. But with a vtable the compiled binary has to remain compatible even with derived classes made later on that can arbitrarily overload the methods. Devirtualization can sometimes overcome this problem if classes are declared final, but it doesn't work that well and also coders often don't bother marking classes final.
std::function, lambdas, concepts etc.
v-tables and virtual are rarely actually required and usually just a convenience tool for doing something in a specific way.
7:04 you can always do these code optimizations when using your own code or have access to source code, but when it's in a library form you just have to use what's given.
Also good to note the performance impact of branch prediction misses. In normal code, the CPU is trying to predict whether a jump is going to happen, but it always knows the destination of the jump. With virtual functions the CPU knows that we will be jumping but it is trying predict the destination of the jump because the address is loaded from memory. When a branch destination is mispredicted, the CPU first needs to load the instructions for the correct branch, and only then it can start loading any data the instructions might require. The performance is also dependent on how predictable the function usage pattern is.
What about polymorphism without inheritance / virtual functions? Which is how polymorphism is usually used IRL.
3:40 did you mean function in derived class which have same signature and name as the one declared `virtual` in a base class?
In derived class can omit `virtual` keyword but in base it is mandatory. Otherwise, you will overload but not override.
Since C++11 best practice is to use keywords `override` and `final` which are written at the end of declaration.
Function which does not have `virtual` keyword in base class is common method, not virtual.
C++ does not have "virtual by default".
And your comparison "fast C code" with "C++ slow code" is actually unfair.
C++ developer could write same "C code" if needed.
Polymorphysm (or other sorts of type erasure) is a tool which should be applied wisely.
Ex. You are sure that two pointer dereferences are neglectably small compared to work done in those virtual functions.
And your code is more maintanable with virtual actually. (Ex. end.)
Respect for taking the time to write such a critical video. A suggestion I make when writing critical things about a programming language is to let clear when the criticism goes on the language itself (e.g. syntax), and when it goes on implementation aspects. Your criticism is on an implementation aspect, that can be optimized by a compiler. So, if you think well, your criticism isn't on (C++'s) polymorphism.
this is purely theorical. in majority of real world programs, the actual function is so much more packed with memory lookups and operations that the single additional pointer dereference's effect on the final performance is insignificant. on the other hand you get a clean and maintainable code. weighing both sides of this scale, my choice personally would be rarely different. in other words, "polymorphism sucks" is very big claim to be fully settled with a 25% performance penalty on a function that has no parameter and does a simple mathematical operation.
1:35: WRONG! Just because a programming language hasn't first class constructions for object orientation, doesn't mean that you cannot program it in a true object oriented fashion. I have programmed object oriented in C. Not easily, nor with any pleasure, but it is possible: in every struct you put pointers to the class struct that contains pointers to the methods, of which the first parameter is a pointer to the "class" that you have defined, a parameter that is normally hidden in the most programming languages, except curiously Python. You can also make pointers to the superclass instance, or implement it otherwise as field copying.
It's the execution pipeline stall/flush/reload that burns the most CPU, not the vtable lookup. However, polymorphism can eliminate huge swaths of complexity in large programs when used appropriately. It's just a tool. Not a panacea
Not you but there seems to be a few people on the Internet who either hate OOP or are just obsessed with raw performance, and this is usually marked by the extensive use and emphasis of edge cases.
In the real world getting working, maintainable code out of the door quickly is far, far more important. If it takes 0.2 seconds longer to execute then most of the time it simply doesn't matter.
Good video though.
"C++ is just a bunch of compiler tricks" - old CS prof. There was a time where C++ was just another pre-processor to a C compiler. Most of the useful parts of C++ can be had in C by using an instance of the "object's" data as the first parameter to each of the "object's" methonds (functions).
I think virtual functions should not be and frequently arent used in hot spots of the code, they are more proper to handle choosing different broad code paths (choosing between exporting to jpg vs png, serial console vs telnet)
I use it for my project, although It's used where performance is not critical, so I never knew they were slow.
Do we get the same performance degradation if we use statically allocated 2D arrays? Because I think the vtables are exactly that - they are pre-computed.
An interesting comparison would've been creating your own vtable-style arrays in C vs. C++ virtual functions.
But yeah, polymorphism is very much overhyped. It does have some nice use cases, but I love the freedoms given by hybrid languages like C++ and python.
Depends on what you mean by "statically allocated 2D arrays". If you were to embed the full vtable in the structure itself, you would waste a lot more memory, but gain performance because you would only need to look up a single function pointer in the structure itself. If you only store a pointer to the vtable in your structure, it would be exactly the same as in the C++ implementation.
@@Cjw9000 yes, I meant a pointer to the vtable. That's why it would be an interesting comparison :-)
You need to keep in mind that historically C++ vtables were better on older systems then they are now, modern systems have made it harder to justify their current existence except when the inner elements of your virtual table functions is the hotpath. Also final pretty much makes them almost as fast as the function call because they get "devirtualized" by the compiler.
you can do OO in C and it would be true OO. All you have to to is the leg work that C++ compiler does on your own (MACROs as very useful in that regard). GObject would be a very good example of that (if not the only one?). A good idea? IMHO no.
In modern C++, one could use a concept for polymorphism, but in Rust switching between a v-table based dynamic polymorphic dispatch and a template based static dispatch is much easier, and with no wrappers.
Yes, I also thought of using CRTP.
I don't think overridden functions are virtual by default. Unless the specs have changed. If you override without using the virtual keyword, it only uses static analysis to determine with method to run. I.e. no v-table.
Yes. You are right, they are not.
I think the title is misleading: Polymorphism is more than just inheritance. A different type of polymorphism is using generics/having type arguments. In Rust, this complexity is resolved ("monomorphized") during compile time, removing the overhead that inheritance would have in this instance. Of course generics are pretty different to work with than virtual methods, but "polymorphism" was the chosen word. Another example could be using interfaces (dynamic dispatch) and traits, that can also be considered polymorphism and in certain languages (say, rust) also have no runtime overhead.
A title that would be less of a problem in my opinion could be "why does inheritance suck for runtime performance?", or "why does inheritance suck?" for short.
A single virtual call is essentially free, it's just a pointer indirection. It's when you have many many calls that you start to see the impact. With modern c++ you can use some template tricks to get rid of it at compile time in most cases
This is not saying that C++ is slower than C. It's just that this is a great explanation of why hand coded switch statements can be faster than virtual functions, both of which you can do in C++ also. Virtual calls certainly need to be eliminated from performance critical areas of your code. In this use case I would use templates rather than virtual functions to achieve the same thing with probably better performance than the C code.
I'm reading a book about C++ written by Bjarne Stroustroup (the creator of C++) and he comments this about virtual functions: "The mechanism of calling virtual functions is almost as efficient as calling 'normal' functions (within 25%), so that efficiency questions shouldn't scare anybody off to use a virtual function, where a normal function call is efficient enough."
The main problem with virtuals is the compiler generally can't inline. That's often a big part of the speed difference. However virtuals are still a good option in many places.
I appreciate how this is actually a technical look at HOW and WHY, rather than just says "this is slow and bad"
Showing the ASM in an understandable way was really nice, and really helps to clarify whats really happening behind the scenes
The performance tradeoff for obeying "more readability by polymorphism" is just not worth it and there's a better perspective on the problem that can help you use other design patterns instead of polymorphism or switch cases.
It would have been nice if you had said something about static dispatch and monomorphization, which languages like Rust do by default. For those that don't know, it creates a copy of you code for each type that you pass, so you get all the benefits of polymorphism without this runtime overhead.
I've actually written a lot of OO code in C. Depending on how you write it, it can be horrific or reasonably pleasant. When I first wrote my data structures library I implemented it in a somewhat generic way with loads of function pointers and size fields and type ID's in nearly everything. It was really easy to work with, but it kind of looked fugly. At some point I implemented some standard types to use as templates, and it was huge. Not that it ran particularly slow or anything, or produced bloated binaries, but it was a lot to maintain. For the second version I just said "fuck it" and implemented raw data copies and had the user provide a void pointer and a size. Let them figure it out.
I suppose you could always try to emulate how the C++ would implement vtables in C. The indirection will always be more slower compared to enum.
edit: try godbolt next time
This omits the fundamental advantage of polymorphism, which is that you can call methods on interfaces without knowing all the possible object types, and extend the application without recompiling. For example, you can have code that calls a method, and the object it is called on is provided by a shared library you load at runtime. The implementation may not even be written at the time that you write the code that uses the interface.
This flexibility comes at a cost. Which is why code that doesn't provide this flexibility can run faster when that flexibility is not required. But try to provide that same flexibility in C, and you end up copying the same vtable mechanism C++ uses, only you have to do it manually instead of the compiler doing it for you.
Polymorphism does not suck. It is a powerful tool, and as such it must be used with care. Learn when to use it, instead of overusing it or swearing it off completely.
It's not polymorphism that sucks, it's this video.
You just don't use polymorphism like that, unless you're a complete beginner learning programming in the 90s. You don't litter your code with useless class hierarchies.
Oh, and you can write OOP code in C, even with something like vtables. It's tedious and ugly and you just don't want to do it (particularly since there's C++ which does it much better).
I once wrote a small C application for a friend using function pointers in structs just to troll them. Good times.
Small point: Shouldn't there be more trials involved in this kind of test than one, you know, for science and stats and all?
Unless that shell stuff involved running the program several times, I couldn't quite figure out what was going on.
I think one misconception is that object-oriented or procedural code is tied to a language. Part of it is the marketing of the languages. Ultimately, all code is compiled to ML even if it is through multiple layers it has to eventually be compiled to ML. No one will say that Assembly (which represents a 1 to 1 with ML) is object-oriented, but the object-oriented code is still implemented in Assembly (or ML). I can write code that is completely procedural is Java just by putting the entire contents of the code in static methods call by the static main function. The code is written in an object-oriented language and has at least one class; however, it is not object-oriented code. Same with C, I can package the data and functions together and use function pointers to create object-oriented code despite the fact that C is a procedural language. Object-oriented, procedural, functional, etc. are programming paradigms and languages can facilitate the use of one or more of those paradigms, but with the exception of a language that completely enforces the use of one of those paradigms (which C does not), they can be implemented using most of the languages out there.
Well, no wonder you think it sucks if you use it that way. I believe that's an overuse. The key point of polymorphism for me is the fact that you can hide implementation and its dependencies, for example to third party libraries, to not pollute the business logic with that third party library and to make that functionality mockable.
Why do programmers always give such random names to features in their languages: “polymorphism”, “lambda function”, “heap”, “stack”, “mutability”. So esoteric and undescriptive. And don’t even get me started on their affinity for acronyms and abbreviations for everything.
I am a hobbiest programmer specializing in nlp. Classes and python are a godsend. The one thing holding me back usually is getting the code down and polymorphism and computionally heavy(as in needing more compute to compile and interpret) are way better for me because i am the bottleneck more than a computer. Also i cant run c code on google colab.
We can just use templates which can be used for compile time inheritance basically
And with the addition of concepts it can be done without gouging your eyes out 😂
It's unfortunate how a lot of videos/resources treat polymorphism and virtual dispatch as synonymous. Polymorphism is a much broader topic, this reasoning really only applies to dynamic/runtime polymorphism and a poor implementation at that.
To be fair, modern (youtube) talks I have watched almost always gravitate towards implementing polymorphism via template metaprogramming techniques, which in turn eliminates runtime dispatching and compile time optimizations, thus achieving max performance.
This video explains polymorphism quite well. I just do not like how it paints it as a massive performance hit. It is just a tool to be used when appropriate, yes it can be overused but so can any tool. When performance becomes a problem i can guarentee that it is probably not polymorphism eating your processing time.
7:00 so are you saying this is only a performance problem when the child’s vtable and related code aren’t loaded into RAM? If that’s the case, if there was a mechanism to indicate to the program which vtables to load into the cache, then in theory the performance difference between the C version would be similar, assuming that the solution using vtables is similar to the C implementation of the example code in this vid.
this video is an example of "the tool X is bad because I can use it wrong on purpose if I want!" reasoning. No C++ programmer would use polymorphism in a real world scenario like this. The best case scenario I can think of is someone saw an educational code sample which had to fit on a single screen, and thought that the example is the recommended usage, not just the syntax.
I think it's a bit wrong to just say "inheritance is bad" as a blanket statement. Rather, you should be aware of the downsides and use it sparingly. Also, I've seen object oriented programming with a sort of "inheritance" done in C. Look at SDL or the Linux kernel source code and it's there. If you want to talk about, say, an input device in a generic way, they just use a struct with function pointers that point to a specific driver's implementation of some generic function. Granted, performance is *probably* slightly better than C++ because you can knock out the vtable and only have a single layer of indirection but it's still not free. The thing to takeaway is only use this when you need it.
soo . . . in C, you can do object-oriented programming, you merely do not get the syntax sugar.
There seem some minor errors in here:
- Methods do not become function pointers, unless they are virtual! Generally they are just regular C-like functions with a *this parameter and name of functions is "mangled" with the class name so linking them at call locations is simple.
- By default child classes of same name / declaration are not virtual. They just "hide" parent class functions if you are not marking them virtual! That is you can call child->operate() and it becomes the overwritten function, but if you have a parent pointer, calling parent->operate() leads to the parent-defined method (if any). This is actually very error-prone and rarely used - but no vtable exists in this case - its all compile time. So it "only works as you hope for if your compiler at the moment see the type info" VS. with virtual it really is dynamic and cuntime polymorphic.
- I would call the first C example also polymorphic - it is the "tagged union" technique and there are countless ways to do polymorphism. This is one runtime polymorphic technique which is faster than the inheritance based OO polymorphism and nearly always better by default - but did not took off much, because when OO was formalized caches were not so big of an issue, but now REALLY are on literally any computer. Other polymorphic techniques exists thoiugh - for example making compile time polymorphism with templates you can write the C++ code to outperform the C codei...
- There are techniques that you can make the compiler "devirtualize" your call. There might be some reasons you HAVE TO write out the word "virtual" but want the good speed. For example marking the class final and not using it through different compilation units (or not on the hot path) usually leads to the compiler being able to devirtualize it or even inline it for you. The issue however is that it takes extra work and skill to make all kinds of compilers do it for you and that it is extra mental effort. Link time optimization can also help, but I practically never use that unless in legacy code, because much better to just properly control what is going on...
I would agree with most of what is in the video. Also would add that in my opinion inheritance based polymorphis should be considered as harmful as GOTO - sometimes I do use GOTO so very rarely I also use this, but very rarely and try not to.... and not just for performance, but because it sphagettifies the code honestly..
Inheritance and runtime polymorphism can be separately useful in all sorts of places, but together they can cause issues if you're not careful and deliberate with their use, even then the applied classes need to be "atomic" as in simplistic and not divisible for your use case. Unfortunately a lot of C++ developers have been around Java and C# too much which destroys the multi-paradigm nature of C++ in their mind and ruins them as developers. I don't think I'd ever consider it as harmful as goto, since goto has killed people and disrupts standard flow control and its not hard to see why it would kill as a result, (and not to mention 99% of those cases is because multi-loop breakouts are otherwise impossible, so its an oversight that has an easy syntax address) whereas polymorphic inheritance is not disruptive to the flow by its nature, if you're using it to do that, its garunteed your first mistake was not polymorphic inheritance, and most of the cost is likely to be in performance, not production quality, though if you want to consider an non-scalable development as "production quality" then sure, but I wouldn't consider it that, it would then only produce developmental hell at the worst.
@@Spartan322 I consider bloat as production quality - and this leads to a lot of bloat.
- There is endless many ways virtual function based inheritance can kill in the same ways: With GOTO its also not the GOTO that kills, but sphagettifying the code so much that you cannot follow where you make a jump and into what state - and this is the key. The jump just happens and shit happens. Unless you code in assembly and goto (jmp) not to a label, but to a memory address in a register / variable, its not the goto that do the error, but the side effect of goto - that you made a mistake where things should jump or what is state expectations there and generally because of total spaghettification you do not know where you are. Now look at very deep inheritance hierarchies and patterns where you pretty much never see the whole picture ever. You cannot even tell what will happen if you call a piece of sh... I mean code. Some of this should be generally an effect of any polymorphism, but honestly I see this coming only mostly in virtual inheritance kind.
- Yes you are right in most of the text, I do consider oop as bad as goto with these virtual inheritance not for the exact same reasons as I consider goto not the best tool. Honestly I had to use goto here and there (for example for a perf optimization for the instruction cache of the hot path: to separate the cold path out with a jump ahead... because relative jumps can have 8bit single-byte parameters as opcode and calls need at least 16 bits so waste more for the cold path + of course the jump way you can spare the call (as the branch you need anyways in the instruction stream). In any ways with goto it was simple to generate the optimal code that took only 2 bytes away from the hot path that would jump out to the cold - then jump back while it would took like 16+24 at the very least if I do a noinlined function call. I find this a very raw use-case. I had to use goto similarily for rare cases over the years and its good its there - but generally bad because its hard to understand what is going on if you overuse it so its not overused.
The most right part of your text is what you say about people who do too much java... I also have seen people want to do java/C#/ts-like reference semantics everywhere thus their code was all with pointers and virtual functions and array of pointers and so on.... This is not only bad for performance, but very bad also for memory safety - and if one wants both, then it becomes really complicated...
Then agaiin inheritance based polymorphism also create random issues when it has interplay with threading... For example I had a team I worked together with who "released the this" in an abstract base-class that I should inherit from in a plugin-based system... The issue is that in their constructor they called some shit that got to know about the this - but object construction was not finished and partial - yet the this pointer was given to some random thread in their constructor. There was no way to make it thread-safe and I had no access to their implementation source codes, just the header.... Funny enough when I explained what is happening (because I literally reverse engineered what they are doing and why I get nasty bugs) they even told me that because its interplay of my code and theirs and they cannot see it in theirs they not gonna fix it.... but omg its very bad practice to leak the this pointer like that and I even pinpointed what thread they gave it to and where it is misused before construction finishes - and this was in a plugin based thing where official docs literally said they made that class to be inherited from. This again also could have killed (it was train control system actually), so I literally rewrote their whole class after reversing it - then fixed the bug by writing a binary compatible variant that we call instead - then made it part of the build pipeline that we do not build if they change the original class........ very nasty..... with no power we could push them to fix in their own code....
@@u9vata I would distinguish between production quality and developmental quality, scalability and long-term ease of development (which is generally distinct though related to maintenance) are completely worthless to a customer generally, and they're not always worthwhile even for you to do, this is another reason it depends on the tool, sometimes a quick and dirty solution is all you're allowed and that's another advantage, though many folks look down on it, I wouldn't discount that fact and I've definitely seen cases of virtual inheritance being adequate for short term development, in some cases speed and memory usage aren't a problem, its pure development time and functionality, so long as the user can reasonably use the application comfortably and it actually works, you're good. Course an issue may arise if a customer also tries to ask for extra elements that aren't exposed in the middle of development, but every paradigm carries that problem to some extent and you are trading dev time for scalability there. Everything is about tradeoffs too.
@@Spartan322 But again - look up how dangerous it can be for example when it interplays with threading via the example I gave - literally could kill people just as well as a badly placed goto. And you cannot leak out the this pointer in parent constructor if there is no parent and no inheritance (of implementation - maybe only interface) in the first place - which is just an example.
Also honestly... GOTO is also only developmental quality because you can totally make good software with it too. As I said its not the GOTO itself that makes the issues - but sphagettified code that developers cannot see bugs of...
@@u9vata Threading is inherently dangerous regardless of what you use, because there is no determinism with threading, you can never expect any order or organization to what you use so it does not matter what paradigm you use, you cannot guarantee an outcome's correctness (in terms of flow or sequence) without extraneous and tedious work, and it is near impossible to track every hole you open unless you figure out how to convert literally every operation into an atomic operation. (which is generally not possible even in the simplest of cases, and even then its not performant and could even cost you more then any other paradigm you could've used) And that's before we consider race conditions, or if you try to prevent race conditions, you might cause a deadlock instead and now your program might be wasting time doing nothing or wasting a thread it can't access, or even just wasting resources keeping a thread alive for no reason. If anything threading is almost (and I literally mean) as deadly as goto, its only because its danger is so well seen that its unlikely that anyone is to step upon that lethality unlike goto, though even then I wouldn't call that rule, all it takes is one person to slip up once with threading, something even easier to do then with goto especially since debugging them is 10 times harder then goto ever was.
It really isn't fair to bring threading into this argument when threading itself is the issue there, more then anything else you could reference, it can make things slower if used wrong which is easy to do, its not easy to intuit even in a simple case, it can break in any number of ways that allow a programatic valid state that should be invalid, it can deadlock the application and break expected sequence of the application in a hard to debug manner, which it is already designed explicitly to do.
I find it very interesting how classes in C++ basically work like they used to with babel before JavaScript had its native classes, where classes were transpiled to an implemetation used prototypes and a WeakMap.
6:56 Why not have the pointer attached to the struct? That way the pointer isn't being looked up, but taken from the structs offset?
what if using in C an array of function pointers instead of switch-case-statement?
What happens if you declare the function arguments and also the operations themselves as const, which they should be, what happens then when you compile with optimizations ? Could you compare the assembly of both ? C++ might surprise you here ;)
While you're right that adding `const` in general has the possibility of improving compiled code, in this contrived example there's no benefit to be had. There's no point in passing the arguments as `const`, since they're passed by value. Declaring the functions themselves as `const` also wouldn't help in this example, where OP is complaining about the indirect virtual call, rather than the compiler's selection of which function to use (which is what `const` helps with).
On a more abstract level, it's also (often) a mistake to declare a virtual base function as `const` unless you're trying to establish a guarantee. A derived implementation (which would also have to be declared `const` to override the base class) might want/need to modify its members for some reason. Sure, it could declare those members as `mutable`, but that has its own philosophical problems.
It's not always a possibility depending on the language and use case, but monomorphization can help a lot.
There is a bit of important context for polymorphism as why it is used and what is it's bad at. Yes, hand coding C for simple operations might be faster, but when you're trying to make a large OO project, doing all those manual things in C can take a long time so it's only worth doing if code performance is very important for the project. In many cases for desktop or web based applications, the extra operations needed for C++ polymorphism are still magnitudes faster than waiting on data transfer betten storage or Internet data. It ends up not really mattering at all for the usability of the program.
This compare apples to oranges, `virtual` should be used when you do not know final types (like you get them form different TU or even DLL).
If you know all types upfront you should use STATIC polymorphism in C++.
C in your example have privilege to know all operations and C++ is deliberately hamstring by hiding all contrite types.
Most funny if you try `qsort` in C you need use effective "virtual" calls where C++ `std::sort` use direct calls that compiler can easy optimize.
Only 20% slower? Hah! These are some rookie numbers.
Write it in python to get 1000% slower on a good day!
The external polymorphism pattern is what you really want there, and if you don't need runtime polymorphism you just make it a template parameter.
I wish I could post links here without the comment disappearing :(