Table of Contents

Concepts are a revolutionary approach for writing templates! They allow you to put constraints on template parameters that improve the readability of code, speed up compilation time, and give better error messages.

Read on and learn how to use them in your code!

What is a concept?  

In short, a concept is a set of constraints on template parameters evaluated at compile time. You can use them for class templates and function templates to control function overloads and partial specialization.

C++20 gives us language support (new keywords - requires, concept) and a set of predefined concepts from the Standard Library.

In other words, you can restrict template parameters with a “natural” and easy syntax. Before C++20, there were various ways to add such constraints. See my other post Simplify Code with if constexpr and Concepts in C++17/C++20 - C++ Stories.

Here’s an example of a simple concept:

template <class T>
concept integral = std::is_integral_v<T>;

The above code defines the integral concept. As you can see, it looks similar to other template<> constructs.

This one uses a condition that we can compute through a well-known type trait (from C++11/C++14) - std::is_integral_v. It yields true or false depending on the input template parameter.

We can also define another one using a requires expression:

template <typename T>
concept ILabel = requires(T v)
{
    {v.buildHtml()} -> std::convertible_to<std::string>;
};

This one looks a bit more serious! But after some time, it seems “readable”:

We defined a concept that requires that an object of type T has a member function called buildHtml(), which returns something convertible to std::string.

Those two examples should give you a taste; let’s try to use them in some real code.

How to use concepts  

In one of the most common case, for a small function template, you’ll see the following syntax:

template <typename T>
requires CONDITION
void DoSomething(T param) { }

You can also use requires clause as the last part of a function declaration:

template <typename T>
void DoSomething(T param) requires CONDITION
{ 
    
}

The key part is the requires clause. It allows us to specify various requirements on the input template parameters.

Let’s look at a simple function template that computes an average of an input container.

#include <numeric>
#include <vector>
#include <iostream>
#include <concepts>

template <typename T> 
requires std::integral<T> || std::floating_point<T>
constexpr double Average(std::vector<T> const &vec) {
    const double sum = std::accumulate(vec.begin(), vec.end(), 0.0);        
    return sum / vec.size();
}

int main() {
    std::vector ints { 1, 2, 3, 4, 5};
    std::cout << Average(ints) << '\n';                                      
}

Play with code @Compiler Explorer

With the above source code, I used two concepts available in the standard library (std::integral and std::floating_point) and combined them together.

One advantage: better compiler errors  

If you play with the previous example and write:

std::vector strings {"abc", "xyz"};
auto test = Average(strings); 

You might get:

<source>:23:24: error: no matching function for call to 'Average(std::vector<const char*, std::allocator<const char*> >&)'
   23 |     auto test = Average(strings);
      |                 ~~~~~~~^~~~~~~~~
<source>:10:18: note: candidate: 'template<class T>  requires (integral<T>) || (floating_point<T>) constexpr double Average(const std::vector<T>&)'
   10 | constexpr double Average(std::vector<T> const &vec) {
      |                  ^~~~~~~

It’s pretty nice!

You can see that the template instantiation failed because your template parameter - const char* is not an integer or floating-point.

Usually, with templates, before the concepts feature, you could get some long cryptic messages about some failed operation that is not possible on a given type in some deep level of the call stack.

Predefined Concepts  

Here’s the list of predefined concepts that we get in C++20 with <concepts> header:

Core language concepts Notes
same_as
derived_from
convertible_to
common_reference_with
common_with
integral
signed_integral
unsigned_integral
floating_point
assignable_from
swappable/swappable_with
destructible
constructible_from
default_initializable
move_constructible
copy_constructible
Comparison concepts Notes
boolean-testable a type can be used in boolean test cases
equality_comparable/equality_comparable_with
totally_ordered/totally_ordered_with Defined in <compare>
three_way_comparable/three_way_comparable_with
Object concepts Notes
movable
copyable
semiregular a type can be copied, moved, swapped, and default constructed
regular a type is both semiregular and equality_comparable
Callable concepts Notes
invocable/regular_invocable
predicate
relation specifies a binary relation
equivalence_relation
strict_weak_order

You can find the list here: Concepts library (C++20) - cppreference.com

And here’s my separate blog post on the Callable concepts:

Code Simplification  

As you could see, the syntax for concepts and constraints is relatively easy, but still, in C++20, we got much more!

There are various shortcuts and terse syntax that allow us to make template code super simple.

We have several things:

  • Abbreviated Function Templates
  • Constrained auto
  • Terse syntax for concepts

For example:

template <typename T>
void print(const std::vector<T>& vec) {
    for (size_t i = 0; auto& elem : vec)
        std::cout << elem << (++i == vec.size() ? "\n" : ", ");
}

We can “compress” it into:

void print2(const std::vector<auto>& vec) {
    for (size_t i = 0; auto& elem : vec)
        std::cout << elem << (++i == vec.size() ? "\n" : ", ");
}

In the above case, I used unconstrained auto. In general, you can write:

auto func(auto param) { }

And it expands into:

template <typename T>
auto func(T param) { }

It looks similar to what we get with C++14 and generic lambdas (Lambda Week: Going Generic).

Additionally, we can also use constrained auto:

void print3(const std::ranges::range auto& container) {
    for (size_t i = 0; auto && elem : container)
        std::cout << elem << (++i == container.size() ? "\n" : ", ");
};

With print3, I removed the need to pass a vector and restricted it for all ranges.

Play with the code @Compiler Explorer

Here we have:

auto func(concept auto param) { }

Translates into:

template <typename T>
requires concept<T>
auto func(T param) { }

What’s more, rather than specifying template <typename T> requires... you can write:

template <std::integral T>
auto sum(const std::vector<T>& vec) {
    // return ...;
}

The requires expression  

One of the most powerful items with concepts is the requires keyword. It has two forms:

  • the requires clause - like requires std::integral<T> or similar
  • the requires expression.

The last one is very flexible and allows to specify quite advanced constraints. In the introduction you’ve seen one case with a detection of buildHtml() member function. Here’s another example:

template<typename T>
concept has_string_data_member = requires(T v) { 
    { v.name_ } -> std::convertible_to<std::string>; 
};

struct Person {
    int age_ { 0 };
    std::string name_;
};

struct Box {
    double weight_ { 0.0 };
    double volume_ { 0.0 };
};

int main() {
    static_assert(has_string_data_member<Person>);
    static_assert(!has_string_data_member<Box>);
}

Play with code @Compiler Explorer

As you can see above, we can write requires(T v), and from now on, we can pretend we have a value of the type T, and then we can list what operations we can use.

Another example:

template <typename T>
concept Clock = requires(T c) { 
    c.start();  
    c.stop();
    c.getTime();
  };

The above concept restricts an “interface” for basic clocks. We require that it has the three member functions, but we don’t specify what type do they return.

From one perspective, we can say that the requires expression takes a type and tries to instantiate the specified requirements. If it fails, then a given class doesn’t comply with this concept. It’s like SFINAE but in a friendly and easy-to-express syntax.

I just showed some basic examples to give you a taste, but look at this article from A. Krzemienski: Requires-expression | Andrzej’s C++ blog which expends this topic in more depth.

The updated Detection Idiom  

Thanks to Concepts we can now easily detect a function, a member function or even a particular overload. This is much simpler that with complicated SFINAE techniques that we had before.

See my other article on that topic: How To Detect Function Overloads in C++17/20, std::from_chars Example - C++ Stories

Compiler Support  

As of May 2021 you can use concepts with all major compilers: GCC (since 10.0), Clang (10.0) and MSVC (2019 16.3 basic support, 16.8 constrained auto, 16.9 abbreviated function templates see notes). Just remember to use appropriate flag for the C++20 standard - -std=c++20/-std=c++2a for Clang/GCC, or /std:c++latest for MSVC.

Summary  

It’s just a tip of an iceberg!

Thanks to the introduction of two new language keywords: requires and concept, you can specify a named requirement on a template argument. This makes code much more readable and less “hacky” (as with previous SFINAE based techniques…).

Additionally, the Standard Library is equipped with a set of predefined concepts (mainly obtained from existing type traits), making it easier to start.

What’s more, C++20 offers even more language features to make the syntax even more compact. It’s mostly due to constrained auto. In some cases, you won’t even need to write template <> at the front of your function template!

What I like about this feature is that you can introduce it slowly in your code. You can add concepts here and there, experiment, see how it works. And then gradually use more advanced constructs and apply in other places.

Back to you

Have you tried concepts? What are your first thoughts on that feature?

What are the most important use cases for you?

Share your comments below the article.

References