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.
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.
To be fair, in C++ that has been the ruling rule of thumb for since ever.
In such cases I prefer using std::variant instead of inheritance, but this works only if all possible types are known ahead of time.
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...
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.
CUDA frameworks, V8, LLVM and GCC, you name it.
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.
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?
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.
To each their own, but yeah, your experience matches mine.
A pedantic Pete would mention C++ has member functions.
Go Pete!
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.)
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.
This is true in C++ also...
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).