MSVC (Microsoft's C++ compiler) had an pretty advanced inter-procedural (LTO) way of doing devirtualization, but it was so buggy and slow that it eventually got disabled. It was trying to prove that a pointer can only target a certain class all the time (or maybe a couple), but things get really messy with typical C/C++ code and even worse once you have DLLs which may inject new derived classes.
Conclusion: devirtualization optimization is so fragile, so that it's better to avoid using virtual calls in performance-critical code to be sure, that no virtual call happens.
It's the same problem with autovectorization. Often the only way to tell whether a compiler optimization is successful is to check the produced assembly or benchmark the code.
It seems that for devirtualization GCC has a warning option -Wsuggest-final-types which is supposed to tell when devirtualization fails in link-time optimization. Not sure how reliable that is, or whether it will produce gobs of unhelpful warnings. Maybe it could be combined with some kind of hint that we want this particular call to be devirtualized, and don't care about calls without the hint.
The big problem with virtual calls is it hinders inlining, but if you design your API correctly that can usually be optimised away by design (put loops inside the call, don't call inside a loop)
Putting loops inside virtual functions can't work in any cases. Like when a heterogenous collection is iterated and a virtual call is performed for each object.
In such cases I prefer using std::variant instead of inheritance, but this works only if all possible types are known ahead of time.
Yeah, no human would work like that. You wouldn't write a sentence for a letter, then go shopping for a single item, then write another sentence, go shopping again, then flush the toilet ...
> Conclusion: devirtualization optimization is so fragile, so that it's better to avoid using virtual calls in performance-critical code to be sure, that no virtual call happens.
To be fair, in C++ that has been the ruling rule of thumb for since ever.
Only if you can actually prove that it matters. If you're always going to the same place, the branch predictor can quickly figure that out even if the compiler can't.
That advice is often given, but I think few C++ programmers follow that to the letter, because you often know beforehand where your performance bottleneck is, and using some simple heuristics upfront may mean you won’t have to spend time benchmarking and refactoring later.
> If you're always going to the same place, the branch predictor can quickly figure that out even if the compiler can't.
But if the compiler knows which function is called, it can choose to inline it, can’t it? For short functions such as getters and setters, the difference can be huge. “call, move, return” instead of “move” is (ballpark) a factor of three, even ignoring cache pressure. The compiler may even choose not to construct a class instance at all, and keep its state in registers.
Branch prediction is not magic and code that relies on it for branches that could otherwise be deduced statically can quickly result in destructive aliasing which results in branch mispredictions even on highly predictable branches.
I ran into a fun crash a year or so ago in the interaction of clang’s profile guided speculative devirtualization and identical code folding (ICF) done by BOLT on the binary.
Clang relied on checking the address of a function pointer in the vtable to validate the class was the type it expected, but it wasn’t necessarily the function that is currently being called. But due to ICF two different subclasses with two different functions shared the same address, so the code made incorrect assumptions about the type. Then it promptly segfaulted.
I also wonder what about devirtualization of dyn traits in Rust. Sure, impl traits like foo<T: AsRef>(bar: T) or foo(bar: impl AsRef) is for sure devirtualized, but foo(bar: &dyn AsRef) or most likely foo(bar: Box<dyn AsRef>), far as I remember, isn't always devirtualized. Sometimes it does, sometimes it doesn't. I wonder if it is MIR that did it, or just completely handed off to LLVM for detection.
Dynamic dispatch seems to be way less popular in Rust than C++. Presumably because it has worse ergonomics (you have to add `Box` everywhere) whereas in C++ the opposite is true (you have to deal with templates).
No. You don't have to add Box everywhere. You do need dynamic storage that is not guaranteed to be known in compile time. Box is just one of them, you can also use "thin box" or "stacked box" or a good ol' "&mut dyn T" would also do great. It basically means a construct of this object with this trait and solved on dynamic dispatch. In C++ that is virtual/pure virtual function.
I think this is unfortunately wishful thinking, a lot of code is still written with traditional inheritance. It just makes unit testing with mocks much easier.
It is a book that plenty think it has Java in it, no it has Smalltalk and C++ and was published before Java came to be as you point out.
The decade was about the book publishing date, rather all the C++ frameworks that came to the market since CFront 2.0 was made available in 1989, and yes it was exactly 7 years, if being pedantic.
First of all they were extremely buggy back then, as anyone that actually used Stepanov's original implementation, from HP with docs hosted at SGI, will remember.
Secondly, Turbo Vision, OWL, VCL, MacApp, PowerPlant, Tools.h++, RogueWave Collection Classes, MFC, wxWidgets, Gtkmm, Qt, PÖET, COM/OLE/ActiveX, CORBA, Copland, BeOS predate Java, not the other way around.
You were criticizing the use of “Java-like”. IMO it gets the point across much better here than “90's C++”. It has nothing to do with what predates what.
It isn't writing C++ as Java, it is writing C++ as it was common between 1990 and 2011, before C++11 and other programming techniques took over in the C++ world, highly influenced by Andrei Alexandrescu's book "Modern C++ Design", published in 2001.
> Maybe just me, but doesn't most C++ code being written today not use inheritance very much, and so make virtual dispatch moot
Most C++ code being written today is the same code that has been here for years. What compels you to believe things are rewritten on the spot? I mean, I personally know of flagship C++ projects that are still stuck in C++14 and earlier. Why would this change?
Perhaps people should pay attention to the fact that C++ goes way out of its way to be backwards compatible.
Well, back when I wrote C++ for a living I tried to keep up with better practices and newish, more compact and sensible language features. So some code I might have written in the 1990s would almost always have overused inheritance, while when adding a feature in the 2010s, I would have minimised such use. I would expect most C++ devs to do something similar today.
> Well, back when I wrote C++ for a living I tried to keep up with better practices and newish, more compact and sensible language features.
That's fine, but definitely something that does not happen in professional settings. You simply don't look at a codebase with a few million LoC and decide on a whim that you are going on to, say, replace all raw pointers with smart pointers. That is not going to happen. That definitely did not happen at all.
May I remind you that Google's C++ coding guidelines discourage exceptions because they do not have the resources to refactor all their code to be exception safe? Google doesn't have the man power but you do?
Not particularly relevant here, but may I point out that Google's C++ guidelines are generally seen as bad, and really only applicable to someone at Google scale, which you are almost certainly not.
> Not particularly relevant here, but may I point out that Google's C++ guidelines are generally seen as bad (...)
I don't have a dog in the race, but this is the first time I ever heard anyone express this opinion. Google's C++ guidelines are the first thing that critics cite when complaining about exceptions, and they do so as an example and not as something "bad".
They also have nothing to do with scale, really. That's a puzzling statement. I have no idea what compels you to make that claim.
The flagships are the laggards usually! They are large, complex projects with lots of ongoing maintenance and commitments. "Upgrading to the new compiler" is seen as tech debt payback... there are deadlines, new features, and more important things to implement!
Upgrading a compiler in production code is always a major task. It often involves introducing breaking changes in dependencies, some of which are far from trivial to fix.
If you tell a Product Manager that you want to upgrade a compiler just because, and all it will take is a month or two of work including full regression tests on all target platforms and collaboration from all teams, that isn't an easy sell.
For comparison, Java will devirtualize calls if the call site typically calls only a single type. To the extent that writing a byte to a ByteBuffer, which looks like five or six virtual calls if you follow the java source code, actually ends up being a single assembly store instruction.
Would Pete get made fun of if he couldn't explain the difference between a member function and a method and why this distinction matters in C++ more than using the term understood everywhere else?
It's not just this, it's everywhere. It's as though C++ is an alternative not to other programming languages but to the entire discipline of Computer Science. The cases where this results in C++ having its own weird names for existing ideas are slightly annoying but the cases where the same terms mean something else quickly become infuriating.
The choice (in C++ and then Java and C#) to pretend that these "member functions" are somehow part of the data structure also more or less guarantees that some students will end up confused about what's actually in the physical representation. Of course the "git gud noob" mindset of C++ doesn't see that as a problem :/
To be fair, C++ isn't alone in this, lets go back into the 1970 and 1980's and compare OOP languages, including failing ones, and FP languages as well, to see how common many of those terms have prevailed to modern times, in what shape and form.
C++ has plenty of warts to complain about, however complaining for the sake of complaining usually doesn't win attention.
I checked how Simula calls its object methods, but it just uses the term procedure as for normal top level functions. At least it shows that the term method is not universal.
I think that C had members, so members that are functions just became that. But then again, members that are types are usually called nested types.
C++ literature, it was common all the way back before the standard came to be.
Example, Turbo C++ manual from 1990, 8 years before the standard.
It is no different from several languages having their own meaning for functor, e.g. it has three different meanings across C++, Haskell and Standard ML/OCaml.
Or what mixins mean between several OOP languages.
Nice post, I had never thought about those tricky ways of proving leafness outside of the obvious "final".
Re "When we know the dynamic type", I made a similar assertion on HN years ago, and of course it turned out that there's a weird wrinkle:
If the code in your snippet is expanded to:
Derived d;
Base *p = &d;
any_external_func(); // Added
p->f();
where any_external_func() is defined in some other translation unit (and, I'm now fairly sure, Derived's ctor is also defined in another translation unit, or it transitively calls something that is), this would seem not to affect anything -- but in fact the compiler must not assume that p's dynamic type is still Derived by the final line. Why? Because the following insane sequence of events might have happened:
1. d's ctor registers its this pointer in some global table.
2. Using this table, any_external_func() calls d's dtor and then overwrites it in place with a fresh instance of Base using placement new, which replaces everything, including its vtable, meaning that p->f() should call Base's version, not Derived's.
(It might be UB to call placement new on static or automatic storage holding an in-lifetime object, I don't know. If so, the above construction is moot -- but the closely analogous situation where we instead dynamically allocate dp = new Derived() still goes through, and is nearly as surprising.)
The compiler is still able to devirtualize and inline the p->f() call, even with an external linkage call preceding it. https://clang.godbolt.org/z/jE3o56ozz
I’ve been programming C++ on and off for over 20 years and have had my moments where I’ve checked on godbolt to make sure classes got devirtualised.
This year, I’ve finally taken the plunge to properly learn Rust (I’ve used it for little things over the years, but never for anything particularly extensive) and one thing that jumped out at me is that you don’t need to think about it, because Rust makes it explicit: everything is statically known unless you explicitly ask for it to be virtual.
[edit: since it wasn’t clear, I mean polymorphism in rust is static by default while in c++ static polymorphism requires relying on the compiler or using templates, otherwise polymorphism is via virtual]
It’s was a little annoying at first because some things don’t just work automatically, but once I got used to it, it was wonderful to never have to think about when the compiler might do something. You also don’t need dynamism most of the time.
I still like tinkering in C++, but I do find you need to know too much about compiler heuristics.
In C++, you can’t have an interface without virtual unless you jump through hoops.
In rust, you can have traits without dyn.
That is, static polymorphism is the default in rust, while in c++ you must jump through hoops for it (eg, see the excellent EnTT’s static polymorphism companion library).
I feel like it’s still missing the point. In a discussion about devitalisation, I thought it was interesting that it’s not something that you have to think about in Rust, while in C++ you do.
I don’t see how that’s relevant when discussing specific language features or semantics, nor how it’s relevant to what I said about Rusts static polymorphism. Nobody is claiming C and C++ didn’t make great contributions or that they aren’t still heavily used.
Fair enough. I didn’t mean to push Rust, which is why I removed the last statement from my original comment. I was really just trying to make an observation that devirtualisation isnt something I need to think about in Rust (you could make an argument that dynamic should be the default and C++ got it right, and it would be an interesting, but different, discussion). I thought that was interesting, as a language design enthusiast.
In the OP, there are ten test cases. Some devirtualise. One only on clang, another only on GCC. In Rust, polymorphism is always "devirtualised" unless I say "no, actually, make this dyn", and in many cases, that's actually perfectly fine. It feels like the rational default, you only pay for the dynamic support when you need it. I don't dislike C++, but sometimes it gets exhausting having to remember all the situations where the compiler might do this or that, or I have to write far more complex code via templates or other techniques to get the behavior I want (sure, its been made easier with recent C++ versions, I admit I'm still on C++20 and even then not all of it; originally because compiler support was patchy, now because I just haven't used C++ as much lately). Its that difference that I thought was interesting.
MSVC (Microsoft's C++ compiler) had an pretty advanced inter-procedural (LTO) way of doing devirtualization, but it was so buggy and slow that it eventually got disabled. It was trying to prove that a pointer can only target a certain class all the time (or maybe a couple), but things get really messy with typical C/C++ code and even worse once you have DLLs which may inject new derived classes.
Conclusion: devirtualization optimization is so fragile, so that it's better to avoid using virtual calls in performance-critical code to be sure, that no virtual call happens.
It's the same problem with autovectorization. Often the only way to tell whether a compiler optimization is successful is to check the produced assembly or benchmark the code.
It seems that for devirtualization GCC has a warning option -Wsuggest-final-types which is supposed to tell when devirtualization fails in link-time optimization. Not sure how reliable that is, or whether it will produce gobs of unhelpful warnings. Maybe it could be combined with some kind of hint that we want this particular call to be devirtualized, and don't care about calls without the hint.
The big problem with virtual calls is it hinders inlining, but if you design your API correctly that can usually be optimised away by design (put loops inside the call, don't call inside a loop)
Putting loops inside virtual functions can't work in any cases. Like when a heterogenous collection is iterated and a virtual call is performed for each object.
In such cases I prefer using std::variant instead of inheritance, but this works only if all possible types are known ahead of time.
Heterogenous containers are a performance killer for all kinds of other reasons.
A vector of variants still wouldn't be optimal. Better to use something like
https://www.boost.org/doc/libs/latest/doc/html/poly_collecti...
Yeah, no human would work like that. You wouldn't write a sentence for a letter, then go shopping for a single item, then write another sentence, go shopping again, then flush the toilet ...
> A vector of variants still wouldn't be optimal
Transpose.
> Conclusion: devirtualization optimization is so fragile, so that it's better to avoid using virtual calls in performance-critical code to be sure, that no virtual call happens.
To be fair, in C++ that has been the ruling rule of thumb for since ever.
Only if you can actually prove that it matters. If you're always going to the same place, the branch predictor can quickly figure that out even if the compiler can't.
> Only if you can actually prove that it matters.
That advice is often given, but I think few C++ programmers follow that to the letter, because you often know beforehand where your performance bottleneck is, and using some simple heuristics upfront may mean you won’t have to spend time benchmarking and refactoring later.
> If you're always going to the same place, the branch predictor can quickly figure that out even if the compiler can't.
But if the compiler knows which function is called, it can choose to inline it, can’t it? For short functions such as getters and setters, the difference can be huge. “call, move, return” instead of “move” is (ballpark) a factor of three, even ignoring cache pressure. The compiler may even choose not to construct a class instance at all, and keep its state in registers.
Branch prediction is not magic and code that relies on it for branches that could otherwise be deduced statically can quickly result in destructive aliasing which results in branch mispredictions even on highly predictable branches.
I ran into a fun crash a year or so ago in the interaction of clang’s profile guided speculative devirtualization and identical code folding (ICF) done by BOLT on the binary.
Clang relied on checking the address of a function pointer in the vtable to validate the class was the type it expected, but it wasn’t necessarily the function that is currently being called. But due to ICF two different subclasses with two different functions shared the same address, so the code made incorrect assumptions about the type. Then it promptly segfaulted.
Did you report it and did it ever get fixed?
I also wonder what about devirtualization of dyn traits in Rust. Sure, impl traits like foo<T: AsRef>(bar: T) or foo(bar: impl AsRef) is for sure devirtualized, but foo(bar: &dyn AsRef) or most likely foo(bar: Box<dyn AsRef>), far as I remember, isn't always devirtualized. Sometimes it does, sometimes it doesn't. I wonder if it is MIR that did it, or just completely handed off to LLVM for detection.
Dynamic dispatch seems to be way less popular in Rust than C++. Presumably because it has worse ergonomics (you have to add `Box` everywhere) whereas in C++ the opposite is true (you have to deal with templates).
No. You don't have to add Box everywhere. You do need dynamic storage that is not guaranteed to be known in compile time. Box is just one of them, you can also use "thin box" or "stacked box" or a good ol' "&mut dyn T" would also do great. It basically means a construct of this object with this trait and solved on dynamic dispatch. In C++ that is virtual/pure virtual function.
In practice you usually pass a Box around though.
Maybe just me, but doesn't most C++ code being written today not use inheritance very much, and so make virtual dispatch moot?
It's still the most convenient way to implement type erasure, so no.
I think this is unfortunately wishful thinking, a lot of code is still written with traditional inheritance. It just makes unit testing with mocks much easier.
Are C++ concepts not enough?
There are codebases out there that don’t use a lot of TMP and prefer Java-like type management.
To each their own, but yeah, your experience matches mine.
90's C++ you mean, a decade before Java came to be, and responsible for famous books like GoF.
The GoF book came out in 94, and Java in 95.
Yes, have I said otherwise?
It is a book that plenty think it has Java in it, no it has Smalltalk and C++ and was published before Java came to be as you point out.
The decade was about the book publishing date, rather all the C++ frameworks that came to the market since CFront 2.0 was made available in 1989, and yes it was exactly 7 years, if being pedantic.
90's C++ already had templates, so “Java-like” is a more succinct descriptor, it doesn’t imply the direction of historical ancestry.
Yes, and?
First of all they were extremely buggy back then, as anyone that actually used Stepanov's original implementation, from HP with docs hosted at SGI, will remember.
Secondly, Turbo Vision, OWL, VCL, MacApp, PowerPlant, Tools.h++, RogueWave Collection Classes, MFC, wxWidgets, Gtkmm, Qt, PÖET, COM/OLE/ActiveX, CORBA, Copland, BeOS predate Java, not the other way around.
You were criticizing the use of “Java-like”. IMO it gets the point across much better here than “90's C++”. It has nothing to do with what predates what.
Yes, because it is completely wrong.
It isn't writing C++ as Java, it is writing C++ as it was common between 1990 and 2011, before C++11 and other programming techniques took over in the C++ world, highly influenced by Andrei Alexandrescu's book "Modern C++ Design", published in 2001.
Only on conference slides maybe, real production code looks quite different, including game engines like Unreal and Godot.
CUDA frameworks, V8, LLVM and GCC, you name it.
> Maybe just me, but doesn't most C++ code being written today not use inheritance very much, and so make virtual dispatch moot
Most C++ code being written today is the same code that has been here for years. What compels you to believe things are rewritten on the spot? I mean, I personally know of flagship C++ projects that are still stuck in C++14 and earlier. Why would this change?
Perhaps people should pay attention to the fact that C++ goes way out of its way to be backwards compatible.
Well, back when I wrote C++ for a living I tried to keep up with better practices and newish, more compact and sensible language features. So some code I might have written in the 1990s would almost always have overused inheritance, while when adding a feature in the 2010s, I would have minimised such use. I would expect most C++ devs to do something similar today.
> Well, back when I wrote C++ for a living I tried to keep up with better practices and newish, more compact and sensible language features.
That's fine, but definitely something that does not happen in professional settings. You simply don't look at a codebase with a few million LoC and decide on a whim that you are going on to, say, replace all raw pointers with smart pointers. That is not going to happen. That definitely did not happen at all.
May I remind you that Google's C++ coding guidelines discourage exceptions because they do not have the resources to refactor all their code to be exception safe? Google doesn't have the man power but you do?
Not particularly relevant here, but may I point out that Google's C++ guidelines are generally seen as bad, and really only applicable to someone at Google scale, which you are almost certainly not.
> Not particularly relevant here, but may I point out that Google's C++ guidelines are generally seen as bad (...)
I don't have a dog in the race, but this is the first time I ever heard anyone express this opinion. Google's C++ guidelines are the first thing that critics cite when complaining about exceptions, and they do so as an example and not as something "bad".
They also have nothing to do with scale, really. That's a puzzling statement. I have no idea what compels you to make that claim.
The flagships are the laggards usually! They are large, complex projects with lots of ongoing maintenance and commitments. "Upgrading to the new compiler" is seen as tech debt payback... there are deadlines, new features, and more important things to implement!
Upgrading a compiler in production code is always a major task. It often involves introducing breaking changes in dependencies, some of which are far from trivial to fix.
If you tell a Product Manager that you want to upgrade a compiler just because, and all it will take is a month or two of work including full regression tests on all target platforms and collaboration from all teams, that isn't an easy sell.
For comparison, Java will devirtualize calls if the call site typically calls only a single type. To the extent that writing a byte to a ByteBuffer, which looks like five or six virtual calls if you follow the java source code, actually ends up being a single assembly store instruction.
Blog post is from 2021, should update the title to include that.
Interesting study though and I wonder if it has at all improved since.
> final method
A pedantic Pete would mention C++ has member functions.
Would Pete get made fun of if he couldn't explain the difference between a member function and a method and why this distinction matters in C++ more than using the term understood everywhere else?
Go Pete!
Whats the difference between a member function and a method?
The C++ standard only ever uses the term "member function" for what other languages call "method", hence why it's pedantic to point it out.
It's not just this, it's everywhere. It's as though C++ is an alternative not to other programming languages but to the entire discipline of Computer Science. The cases where this results in C++ having its own weird names for existing ideas are slightly annoying but the cases where the same terms mean something else quickly become infuriating.
The choice (in C++ and then Java and C#) to pretend that these "member functions" are somehow part of the data structure also more or less guarantees that some students will end up confused about what's actually in the physical representation. Of course the "git gud noob" mindset of C++ doesn't see that as a problem :/
To be fair, C++ isn't alone in this, lets go back into the 1970 and 1980's and compare OOP languages, including failing ones, and FP languages as well, to see how common many of those terms have prevailed to modern times, in what shape and form.
C++ has plenty of warts to complain about, however complaining for the sake of complaining usually doesn't win attention.
I checked how Simula calls its object methods, but it just uses the term procedure as for normal top level functions. At least it shows that the term method is not universal.
I think that C had members, so members that are functions just became that. But then again, members that are types are usually called nested types.
C++ literature, it was common all the way back before the standard came to be.
Example, Turbo C++ manual from 1990, 8 years before the standard.
It is no different from several languages having their own meaning for functor, e.g. it has three different meanings across C++, Haskell and Standard ML/OCaml.
Or what mixins mean between several OOP languages.
Nice post, I had never thought about those tricky ways of proving leafness outside of the obvious "final".
Re "When we know the dynamic type", I made a similar assertion on HN years ago, and of course it turned out that there's a weird wrinkle:
If the code in your snippet is expanded to:
where any_external_func() is defined in some other translation unit (and, I'm now fairly sure, Derived's ctor is also defined in another translation unit, or it transitively calls something that is), this would seem not to affect anything -- but in fact the compiler must not assume that p's dynamic type is still Derived by the final line. Why? Because the following insane sequence of events might have happened:
1. d's ctor registers its this pointer in some global table.
2. Using this table, any_external_func() calls d's dtor and then overwrites it in place with a fresh instance of Base using placement new, which replaces everything, including its vtable, meaning that p->f() should call Base's version, not Derived's.
(It might be UB to call placement new on static or automatic storage holding an in-lifetime object, I don't know. If so, the above construction is moot -- but the closely analogous situation where we instead dynamically allocate dp = new Derived() still goes through, and is nearly as surprising.)
The compiler is still able to devirtualize and inline the p->f() call, even with an external linkage call preceding it. https://clang.godbolt.org/z/jE3o56ozz
I’ve been programming C++ on and off for over 20 years and have had my moments where I’ve checked on godbolt to make sure classes got devirtualised.
This year, I’ve finally taken the plunge to properly learn Rust (I’ve used it for little things over the years, but never for anything particularly extensive) and one thing that jumped out at me is that you don’t need to think about it, because Rust makes it explicit: everything is statically known unless you explicitly ask for it to be virtual.
[edit: since it wasn’t clear, I mean polymorphism in rust is static by default while in c++ static polymorphism requires relying on the compiler or using templates, otherwise polymorphism is via virtual]
It’s was a little annoying at first because some things don’t just work automatically, but once I got used to it, it was wonderful to never have to think about when the compiler might do something. You also don’t need dynamism most of the time.
I still like tinkering in C++, but I do find you need to know too much about compiler heuristics.
> everything is statically known unless you explicitly ask for it to be virtual.
This is true in C++ also...
In C++, you can’t have an interface without virtual unless you jump through hoops.
In rust, you can have traits without dyn.
That is, static polymorphism is the default in rust, while in c++ you must jump through hoops for it (eg, see the excellent EnTT’s static polymorphism companion library).
Yes you can, you only need to adopt modern C++ with concepts, compile time execution and now compile time reflection.
I feel like it’s still missing the point. In a discussion about devitalisation, I thought it was interesting that it’s not something that you have to think about in Rust, while in C++ you do.
Of course you do, it depends on how the application architecture looks like.
I also have my share of hobby coding in Rust.
While Rust is a good improvement, without C++ there isn't Rust compiler, at least for the forseable future.
I don’t see how that’s relevant when discussing specific language features or semantics, nor how it’s relevant to what I said about Rusts static polymorphism. Nobody is claiming C and C++ didn’t make great contributions or that they aren’t still heavily used.
It is a heads up given how many Rusties downplay the role of C++ in the industry, including their own compiler.
Fair enough. I didn’t mean to push Rust, which is why I removed the last statement from my original comment. I was really just trying to make an observation that devirtualisation isnt something I need to think about in Rust (you could make an argument that dynamic should be the default and C++ got it right, and it would be an interesting, but different, discussion). I thought that was interesting, as a language design enthusiast.
In the OP, there are ten test cases. Some devirtualise. One only on clang, another only on GCC. In Rust, polymorphism is always "devirtualised" unless I say "no, actually, make this dyn", and in many cases, that's actually perfectly fine. It feels like the rational default, you only pay for the dynamic support when you need it. I don't dislike C++, but sometimes it gets exhausting having to remember all the situations where the compiler might do this or that, or I have to write far more complex code via templates or other techniques to get the behavior I want (sure, its been made easier with recent C++ versions, I admit I'm still on C++20 and even then not all of it; originally because compiler support was patchy, now because I just haven't used C++ as much lately). Its that difference that I thought was interesting.
Point taken, in that regard yeah, different design decisions are always interesting to look into.