Daniel Lemire's blog

, 14 min read

Defining interfaces in C++ with ‘concepts’ (C++20)

18 thoughts on “Defining interfaces in C++ with ‘concepts’ (C++20)”

  1. Martin Cohen says:

    There are a couple of weird grammatical errors in this post, including the very first sentence. Makes me wonder how it was written.

    1. I am sorry Martin for the poor grammar.

      1. Someone says:

        At least, we know it wasn’t chatGPT 😏

        1. All my future blog post will be written by ChatGPT. I promise.

    2. Lucio Zanette says:

      For those who perhaps use English as a second line, the article is very well written. I’m a brazilian and could read without any problems. The goal is to be able to communicate.
      And between us, I still haven’t found the error in the very first line.

  2. David Shin says:

    C++ concepts fill a need in the language, and I am happy they have finally arrived. However, in my opinion, the execution was not great. I would have preferred something that minimizes the syntax-delta between a concept definition and a class that implements that concept. Java’s interface is superior by this measure.

    To illustrate, suppose I want to express a concept for a class that has a const foo() method that accepts a non-const int reference as a parameter. How do I do this? It would be great if I could simply do:

    // hypothetical concepts syntax
    concept MyConcept {
    void foo(int&) const;
    };

    Instead the simplest way that I am aware of to express this concept looks like this:

    template<typename T>
    concept MyConceptRefOrNonRef = requires(const T t) {
    t.foo(int{});
    };

    template<typename T>
    concept MyConceptRefOnly = requires(const T t, int i) {
    t.foo(i);
    };

    template <typename T>
    concept MyConcept = MyConceptRefOnly<T> && !MyConceptRefOrNonRef<T>;

    Hardly intuitive!

    This shows how cumbersome it is to write your own concepts to specify exactly what you want. Furthermore, as you point out in your last paragraph, the purpose of concepts is to serve as documentation. The fact that so many lines are needed to express such a simple constraint indicates that the syntax is not well optimized for its purpose.

    1. The following looks reasonable to me:

      template<typename T>
      concept eatable = requires(T v, int i) {
          {  v.eat(i) };
      } && !requires(T v) {
          {  v.eat(int{}) };
      };
      
      1. David Shin says:

        That is more concise, but it is essentially the same as my solution. Mine tries to provide more clarity my naming the two sub-concepts according to their purpose, but without that sub-naming, simplifies to yours.

        If you add more similarly-constrained parameters to the method, or add more similarly-constrained methods to the class, the concept definition becomes quite unwieldy, compared to the equivalent Java interface definition.

        1. If you want to stipulate exactly the signature in C++, you can use (multiple) inheritance. Note that there is no canonical way to solve your problem in Java… e.g., for a class that has a const foo() method that accepts a non-const int reference as a parameter.

          1. David Shin says:

            Yes, inheritance (along with a static_assert with std::derived_from or similar) does provide a way to statically declare requirements of a template class parameter. This does have some shortcomings, like if you want to declare static methods or impose requirements on inner classes. Besides such shortcomings, I dislike this usage of inheritance, as it gives the false impression to the reader of the code that there is dynamic dispatch going on.

            To give a motivating real-world example where I expect concepts to come into play: suppose I want to write my own version of std::vector, which similarly takes an Alloc class template parameter. I want to declare a concept for this Alloc class to match std::vector’s. As I’m writing my code, I want my IDE to show me all member variables/functions that are guaranteed to exist for this Alloc class. I also want my compiler to complain to me if I make any illegal assumptions about this class, even if they happen to be valid for the particular class instantiation that I happen to be using. Theoretically, concepts should be the right tool for this job. Practically, it’s so difficult to express the requirements of the Alloc template class parameter in the language of C++20 concepts, that to my knowledge nobody has done it. Multiple inheritance won’t help express requirements like Alloc::rebind.

            I agree that the Java-interface analogy only goes so far. To be more correct, I should invoke some of the C++0x concepts proposals – there were approaches here that were closer to what I wish for, which I believe would have made defining the Alloc concept more feasible.

  3. Nathan Myers says:

    Concepts are rather more powerful than suggested.

    E.g., you can overload on concept matching, so you might have different template implementations according to what facilities the types offer.

    You can also use a “requires” clause as a predicate to try out if an expression is defined, avoiding dreaded “template metaprogramming”.

  4. Jeff Creswell says:

    Why not just inherit from a superclass with pure virtual functions? What are the limitations of inheritance that concepts solve?

    1. Nathan Myers says:

      Virtual functions are a runtime-choice phenomenon. Template work all happens at compile time. Functions in templates and lambdas get merged together inline and optimized for the particular call site, eliminating abstraction overhead. Templates that match differently can involve completely different types and interfaces: see std::visit applied to std::variant.

      A good program uses inheritance sparingly, and virtual functions moreso. There are certainly places for them, but they arise rarely. (It was deeply silly for Java to make them the default.) In particular, virtual functions are inherently an implementation detail, so a class with public virtual functions is one that is not doing much, if any, abstraction work. It is OK to use them just as mechanism, as a sort of structured function pointer, so not providing abstraction is no sin if the purpose is clear.

      But the heavy lifting should happen at compile time, in templates where types are all known and bugs are exposed before testing starts.

  5. Bianca Jones says:

    As more and more complexity is added to C++, me and my team (writing code in industry) find ourselves moving back toward code that resembles plain old C.

    We can’t afford to spend minutes, hours, or even days wrangling all of these new C++ features, assuming we even find the time to properly learn them in the first place.

    While we don’t like giving up classes, objects, RAII, exceptions, and the other benefits that the core of C++ can bring, at least C is something that everybody on the team can understand to a suitable level.

    And, no, we will never begin using Rust. Rust makes complex C++ look appealing.

    1. Thanks Bianca. I should stress that I like C. When I write about new C++ features, it is not as a way to pressure people into using them. There is nothing wrong with using very basic C++, or just C.

    2. Nathan Myers says:

      If your needs are limited, C is often good enough. Likewise Python, or JS.

      Nobody criticizes you for using a shovel except where a backhoe would have been the smarter choice.

    3. Simion J says:

      You talking about C11 or C99 as base for your team? And what about compiler choices? 🙂
      Thanks

  6. Lucio Zanette says:

    Second language*
    The grammar checker got me!