Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Defining interfaces in C++: concepts versus inheritance (lemire.me)
64 points by jandeboevrie on April 20, 2023 | hide | past | favorite | 29 comments


I've done some pretty involved concept work. I made this CBOR implementation that heavily uses concepts: https://github.com/absperf/conbor/blob/main/conbor/cbor.hxx (Warning: I abandoned this mid-development. It "worked" for most of my uses, but many things were dropped in mid-development)

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.


> Given an optimizing compiler, the first function (count(a)) is likely to just immediately return the size of the backing vector. The function is nearly free.

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...


That's true but devirtualization optimizations tend to be pretty brittle and it's very easy to fall of the optimizer's blessed path and end up back to doing to a virtual call without realizing it.

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.


Unless you are in a hot loop (where you may not use virtual methods to begin with), I don’t think that performance difference is significant. Virtual calls have a slight overhead, but far from serious, and similarly not inlining something that you call only a single time for example is not the end of the world.


The problem with not inlining is less with the overhead of the function call itself, and more the loss of further optimization opportunities. Consider this (trivial) example:

    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.


> 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.


Yes, in principle a compiler is free to generate arbitrarily horrendous code regardless of what you ask it to do.

In practice, non-virtual function calls are reliably compiled to fairly efficient code while virtual calls are much less reliable.


> 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.


So has programmer methodology. Throwing virtual calls everywhere is a relic of the past from the era of OOP fetishism. If you actually have statically verifiable leafs for virtual calls then you didn't even need a virtual call to begin with. It's code slop.


Strongly agree on the philosophy - but alas my experience with this specific case has not been great.

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.


"So what are concepts good for? I think it is mostly about documenting your code".

I think the purpose of concept is to add constraints to help the compiler to do static checking instead of just 'documenting your code'.


Concepts seem great, but do they/will they always depend on using templates? Because I try to use templates as a last ditch escape hatch due to debugging/error messages being gnarly.

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.


Concepts improve the situation with error messages for templates. You get an error message saying the equivalent of "You can't pass this argument to this template function because type X doesn't match concept Y" rather than the esoteric one you get now.


> Concepts seem great, but do they/will they always depend on using templates?

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?


One of the main points of concepts is to make template error messages much better, and for the most part it's true. Concepts error messages still suck, but they tend to suck much less than non-concept template error messages.


Thanks! Seriously any suckage reduction no matter how minuscule is a vast improvement over current template errors. At my rate I'll be on C++20 by 2025 so looking forward to it.


"Given an optimizing compiler, the first function (count(a)) is likely to just immediately return the size of the backing vector. The function is nearly free."

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.


Its only more complex for the programmer because of presumed experiance with polymorphism. Its alot easier to debug a concept because a type either has the function or not there is no overloading or massive inheritance tree where u can easily mistake what is actually being called.


If I remove the inheritance, GCC trunk figures it out at -O2 or above (Clang doesn't figure it out). If I add the inheritance back in, GCC doesn't manage either.

https://godbolt.org/z/7bTETsdd1


Clang does figure it out. Note that clang has a branch, not a loop. It jumps to returning 0, else it continues to do the same thing gcc does. I'd say it figured it out, even if the code isn't as pretty.

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


You're right, I should have read the Clang output more closely. The part about final inheritance makes sense, too, because otherwise a subclass could override the behaviour and count_concept() would have to support that.


https://godbolt.org/z/4WxGj7c65

Sure looks to me like GCC figures it out. Clang doesn't seem to, though.


I haven't written C++ in many years, so I don't feel qualified to comment on C++.

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.


I think you're referring to the SR-103 issue: https://github.com/apple/swift/issues/42725


That looks like it!

Thanks!


Recent and related:

Defining interfaces in C++ with ‘concepts’ (C++20) - https://news.ycombinator.com/item?id=35624899 - April 2023 (73 comments)


I wonder if the author intended to use private inheritance?


Structs default to public inheritance. I don’t think the usage of struct here makes sense though, since there’s an invariant to uphold between the index and the ”std::vector<T> array” (sic).


No. Straight from the 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.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: