Defining interfaces in C++: concepts versus inheritance
lemire.me
lemire.me
I ended up abandoning it because recursive concepts weren't universally functioning (clang didn't support them right), and more nefariously, the C++ name definition rules made a whole lot of things very very painful. concepts were evaluated based on their definition site, not based on names available where they were expanded. This is true of templates too, but in concepts, it prevented me from allowing users to effectively extend my concepts-based library, and some circularly-dependent concepts were impossible without a dummy "Adl" type to allow ADL to function. There are a lot of little places where the extent to which concepts work is entirely dependent on their order.
In simple cases, concepts allow you to do things similarly to Rust traits and get some of those powers, but in C++, a lot of little frustrations keep it from being as ergonomic or as powerful, and you still have to play stupid C++ name lookup games.
I remember a LOT of little annoyances, as well, in trying to partially constrain concepts, or express recursive concepts (which are very necessary for parsing a recursive format like CBOR). I tried a simple JSON parser that only allowed arrays and strings using C++ concepts and hit the same kinds of problems.
I'd recommend using concepts, I think, but keep in mind that trying to do anything at all generic or extensible might cause you to do more C++ detangling than you want.
The compiler is able to do that with count_inheritance() as well if it's able to prove which instance of iter_base is used in the call. I suppose even many experienced C++ developers are not aware of this. This optimization is known as "devirtualization" and is fairly well-implemented in Clang and GCC. It's even more effective since the advent of LTO. Some more info: https://quuxplusone.github.io/blog/2021/02/15/devirtualizati... https://blog.llvm.org/2017/03/devirtualization-in-llvm-and-c...
Worse, once the devirtualization optimization has failed, any further optimizations you would get from inlining the call will also fail.
If you're programming in C++, you probably do care about this level of performance, and in that case, it's nice to program in a style that guarantees it instead of hoping for a sufficiently smart compiler.
Neither implementation guarantees any particular sequence of assembly instructions. Both require hoping that a sufficiently smart compiler will compile it to a sufficiently optimal sequence of instructions.
In practice, non-virtual function calls are reliably compiled to fairly efficient code while virtual calls are much less reliable.
Like I said, this echoes the conventional wisdom that most C++ developers seem to retain. The compiler landscape has changed since that wisdom was formed, since the advent of LTO and devirtualization optimizations.
I’ve seen it simplify patches of code here and there, and that does apply to the trivial examples given in the post this topic links to - a function call involving a known child class. But add some basic real-world complexity and it quickly gets too complex for the optimiser to prove that it knows for sure what child class it is dealing with.
main() {
int x = foo() + 3;
}
int foo() {
return 5;
}
Without inlining you have both the overhead of the call and the arithmetic addition. If you can inline the call then you get: main() {
int x = 5 + 3;
}
But more importantly, the optimizer can now also eliminate the addition too: main() {
int x = 8;
}
This is obviously a trivial example, but in real-world code, the optimization options opened up after inlining are important.I think the purpose of concept is to add constraints to help the compiler to do static checking instead of just 'documenting your code'.
I'm slowly working my way through the C++ versions (I prefer to use things that are battletested) and mostly loving it except that compiler error messages are just getting more and more esoteric. Or rather, the error messages that have always been esoteric are getting more common since so much of modern C++ features rely on them.
I don't get your question. By definition, C++'s concepts are requirements specified on template arguments. Do you foresee any scenario where requirements on template arguments should not be used with templates?
As far as I can see, neither gcc nor clang actually do this (unless the array has a fixed size; and even then in the gcc case only if the array is zero or one element long).
Probably still more efficient than a virtual call, but also a lot more complex on both the programmer and the compiler, and a lot harder to debug.
I tried using `final` to get them to figure out the inheritance case, but devirtualization didn't work when inlining a method defined on a base class. https://godbolt.org/z/h4GEYnzMo
Sure looks to me like GCC figures it out. Clang doesn't seem to, though.
However, I have been writing Swift, every single day, since June, 2014, so I may have a bit more authority, there.
In this post, I describe a rather subtle bug that can happen, because of the differences between inheritance and interface: https://littlegreenviper.com/miscellany/swiftwater/the-curio...
It's actually something that happens to me fairly often, because I do mix them. I have learned to recognize it, when it happens. The best "cure," is to not use protocol defaults for properties and functions that I want to be implemented by inheritance hierarchies.
Thanks!
Defining interfaces in C++ with ‘concepts’ (C++20) - https://news.ycombinator.com/item?id=35624899 - April 2023 (73 comments)
Private inheritance means you don’t get the base class interface as your own in the derived class. It is not an “is a” relation. It’s more of a “has a” relation.