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

In an earlier blog post, I showed that the Go programming language allows you to write generic functions once you have defined an interface. Java has a very similar concept under the same name (interface). I gave the following example:

type IntIterable interface {
    HasNext() bool
    Next() uint32
    Reset()
}

func Count(i IntIterable) (count int) {
    count = 0
    i.Reset()
    for i.HasNext() {
        i.Next()
        count++
    }
    return
}

From this code, all you have to do is provide a type that supports the interface (having the methods HasNext, Next and Reset with the right signature) and you can use the function Count.

What about C++? Assume that I do not want to use C++ inheritance. In conventional C++, you could just write a Count template like so:

template <class T> size_t count(T &t) {
  t.reset();
  size_t count = 0;
  while (t.has_next()) {
    t.next();
    count++;
  }
  return count;
}

That is fine when used in moderation, but if I am a programmer and I need to use a template function I am unfamiliar with, I might have to read the code to find out what my type needs to implement to be compatible with the function template. Of course, it also limits the tools that I use to program: they cannot much about the type I am going to have in practice within the count function.

Thankfully, C++ now has the equivalent to a Go or Java interface, and it is called a concept (it requires a recent compiler with support for C++20). You would implement it as so…

template <typename T>
concept is_iterable = requires(T v) {
                        { v.has_next() } -> std::convertible_to<bool>;
                        { v.next() } -> std::same_as<uint32_t>;
                        { v.reset() };
                      };

template <is_iterable T> size_t count(T &t) {
  t.reset();
  size_t count = 0;
  while (t.has_next()) {
    t.next();
    count++;
  }
  return count;
}

It is even better than the Go equivalent because, as my example demonstrate, I do not have to require strictly that the has_next function return a Boolean, I can just require that it returns something that can be converted to a Boolean. In this particular example, I require that the next method returns a specific type (uint32_t), but I could have required, instead, to have an integer (std::is_integral<T>::value) or a number (std::is_arithmetic<T>::value).

In Go, I found that using an interface was not free: it can make the code slower. In C++, if you implement the following type and call count on it, you find that optimizing compilers are able to just figure out that they need to return the size of the inner vector. In other words: the use of a concept/template has no runtime cost. However, you pay for it up-front with greater compile-time.

struct iterable_array {
    std::vector<uint32_t> array{};
    size_t index = 0;
    void reset() { index = 0; }
    bool has_next() { return index < array.size(); }
    uint32_t next() { index++; return array[index - 1]; }
};

size_t f(iterable_array & a) {
    return count(a);
}

In C++, you can even make the runtime cost absolutely nil by forcing compile-time computation. The trick is to make sure that your type can be instantiated as a compile-time constant, and then you just pass it to the count function. It works with a recent GCC right now, but should be eventually broadly supported. In the following code, the function just returns the integer 10.

template <is_iterable T>
constexpr size_t count(T&& t) {
    return count(t);
}

struct iterable_array {
    constexpr iterable_array(size_t s) : array(s) {}
    std::vector<uint32_t> array{};
    size_t index = 0;
    constexpr void reset() { index = 0; }
    constexpr bool has_next() { return index < array.size(); }
    constexpr uint32_t next() { index++; return array[index - 1]; }
};


consteval size_t f() {
    return count(iterable_array(10));
}


You can examine my source code if you would like.

So what are concepts good for? I think it is mostly about documenting your code. For example, in the simdjson library, we have template methods of the type get<T>() where T is meant to be one of a few select types (int64_t, double, std::string_view, etc.). Some users invariably hope for some magic and just do get<mytypefromanotherlibrary>() hoping that the simdjson will somehow have an adequate overload. They then get a nasty error message. By using concepts, we might limit these programming errors. In fact, IDEs and C++ editor might catch it right away.

In the next blog post, I explain why concepts might be preferable than standard inheritance.

Published by

Daniel Lemire

A computer science professor at the University of Quebec (TELUQ).

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

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

    1. 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. 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. 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. 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. 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. Why not just inherit from a superclass with pure virtual functions? What are the limitations of inheritance that concepts solve?

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

Leave a Reply

Your email address will not be published.

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax

You may subscribe to this blog by email.