[HN Gopher] Defining interfaces in C++: concepts versus inheritance
       ___________________________________________________________________
        
       Defining interfaces in C++: concepts versus inheritance
        
       Author : jandeboevrie
       Score  : 53 points
       Date   : 2023-04-20 19:03 UTC (3 hours ago)
        
 (HTM) web link (lemire.me)
 (TXT) w3m dump (lemire.me)
        
       | synergy20 wrote:
       | "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'.
        
       | ChrisMarshallNY wrote:
       | 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.
        
         | meindnoch wrote:
         | I think you're referring to the SR-103 issue:
         | https://github.com/apple/swift/issues/42725
        
           | ChrisMarshallNY wrote:
           | That looks like it!
           | 
           | Thanks!
        
       | dang wrote:
       | Recent and related:
       | 
       |  _Defining interfaces in C++ with 'concepts' (C++20)_ -
       | https://news.ycombinator.com/item?id=35624899 - April 2023 (73
       | comments)
        
       | [deleted]
        
       | butterisgood wrote:
       | I wonder if the author intended to use private inheritance?
        
         | nemetroid wrote:
         | 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).
        
         | deweywsu wrote:
         | [dead]
        
       | bramblerose wrote:
       | "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.
        
         | crickey wrote:
         | 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.
        
         | nemetroid wrote:
         | _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
        
           | celrod wrote:
           | 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
        
         | kllrnohj wrote:
         | https://godbolt.org/z/4WxGj7c65
         | 
         | Sure looks to me like GCC figures it out. Clang doesn't seem
         | to, though.
        
       | klik99 wrote:
       | 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.
        
         | bobbyi wrote:
         | 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.
        
         | Taywee wrote:
         | 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.
        
       | Taywee wrote:
       | 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.
        
       | CyberRabbi wrote:
       | > 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...
        
         | munificent wrote:
         | 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.
        
           | CyberRabbi wrote:
           | > 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.
        
             | munificent wrote:
             | 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.
        
               | CyberRabbi wrote:
               | > 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.
        
       ___________________________________________________________________
       (page generated 2023-04-20 23:02 UTC)