C++ Concepts — make the templates great(er) again!

Year 2020 was undoubtedly a year of dynamic changes for all of us. It will be remembered by many as a year full of epochal events, unfortunately not always positive. As the saying goes, every change is good if cared right. The result of the changes that took place at that time, that was the most noticeable by the business, was the transition to the remote work mode. Adopt, improvise, overcome. Some people think it’s good, others think it’s bad, and this topic let alone could be so fruitful that it can get separated blog post.

However, I did not want to write about politics or business. It turns out that 2020 also brought changes in the C ++ world. At the end of that year, a new standard of this language was published. It debuted on December 15, 2020. It includes many new big features such as coroutines, concepts, likely and unlikely attributes or immediate functions. However, we will focus on concepts and its impact on modern C++ in this article.

Templates are the basis of general programming in C++. As strongly typed, C++ requires all variables to be of a specific type. However, many data structures and algorithms look the same regardless of the type they run on. Templates enables developer to provide logic and operations to perform without constraint the types at definition level in any way Yet this feature is extremely useful, it far from being perfect for that very reason.

template <typename T>
T minimum(const T& x, const T& y)
{
return (x< y)? x : y;
}

Concepts are new approach for writing templates inc C++ code. They allow you to put constraints on template parameters. It improves readability, speed up compilation time and what may please many C++ developers — give better error messages.

What are concepts?

Basically, the concepts are a set of constraints on template parameters that are evaluated at compile time and can be used to control overloading and partial specialization in class templates and function templates. Sounds complicated but it is because of general definition. It will get clearer as we go.

Developers are receiving new keywords to work with concepts — requires and concept and a bunch of predefined concepts in the Standard Library.

Example speaks more than 1000 words, so lets take a look:

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

The above gist defines “integral” concept. We are using known since C++17 type traid std::is_integral_v. It returns true (for integrals) or false depending on template input.

The other way of defining concept is via requires expression, which allows us to use more sophisticated expressions:

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

We defined a concept that requires that an object T has a function foo that returns something that is convertible to string. Simple as that! I bet you can see how powerful this is.

I like to see concepts as something between bare templates and specialization.

How to use them?

Time to code. Typical wireframe for small template functions using concepts will look like this:

template <typename T>
requires CONDITION
void foo(T v) { }

OR

template <typename T>
void foo(T v) requires CONDITION{}

With this knowledge, we should be able to write some real code!

#include <concepts>

template <typename T>
concept addable = requires (T a, T b) {
a+b;
};
void foo(addable auto x) {}

int main() {
foo(2); // <- ok
// foo("iteo"); <- compilation error
return 0;
}

Template errors as the most frightening thing in C++

I promised that errors when using concepts are not as scary as in pure templates. Let’s take a look what compilation error we would get if the line.

<source>:12:8: error: no matching function for call to 'foo(const char [5])'
12 | foo("iteo");// <- error
| ~~~^~~~~~~~
<source>:7:6: note: candidate: 'template<class auto:1> requires addable<auto:1> void foo(auto:1)'
7 | void foo(addable auto x) {}
| ^~~
<source>:7:6: note: template argument deduction/substitution failed:
<source>:7:6: note: constraints not satisfied
<source>: In substitution of 'template<class auto:1> requires addable<auto:1> void foo(auto:1) [with auto:1 = const char*]':
<source>:12:8: required from here
<source>:4:9: required for the satisfaction of 'addable<auto:1>' [with auto:1 = const char*]
<source>:4:19: in requirements with 'T a', 'T b' [with T = const char*]
<source>:5:6: note: the required expression '(a + b)' is invalid
5 | a+b;
| ~^~

It’s pretty neat! As you can see, the compiler informed us that compilation failed because our argument [const char*] did not satisfy the constraint made by expression a+b in our addable concept definition. Even thought for some it still may not look the best (like come on, python would point at exact place what to fix and even propose a solution) but this is much better than it would be without concepts. Instead of long, cryptic messages about some failed operation that are not possible on the provided type with unreadable deep level call stack dump we are getting exact reason what went wrong. Big improvement.

Concepts library

Not all concepts has to be defined by developer. STL provides a lot of ready to use predefined concepts

Core concpets :

  • same_as — specifies two types are the same.
  • derived_from — specifies that a type is derived from another type.
  • convertible_to — specifies that a type is implicitly convertible to another type.
  • common_with — specifies that two types share a common type.
  • integral — specifies that a type is an integral type.
  • default_constructible — specifies that an object of a type can be default-constructed.

Comparison concepts:

  • boolean — specifies that a type can be used in Boolean contexts.
  • equality_comparable — specifies that operator== is an equivalence relation.

Object concepts:

  • movable — specifies that an object of a type can be moved and swapped.
  • copyable — specifies that an object of a type can be copied, moved, and swapped.
  • semiregular — specifies that an object of a type can be copied, moved, swapped, and default constructed.
  • regular — specifies that a type is regular, that is, it is both semiregular and equality_comparable.

Callable concepts:

  • invocable — specifies that a callable type can be invoked with a given set of argument types.
  • predicate — specifies that a callable type is a Boolean predicate.

Here you can find a full list: https://en.cppreference.com/w/cpp/concepts

Summary

This is just a tip of an iceberg!

Thanks to two new keywords requires and concept, developers can specify a named requirement of template argument. It makes code more readable and less hacky or error-prone.

Furthermore, STL comes with a set of predefined concepts (mostly derived from already existing type traits), so getting started is easy.

The great thing about this feature is that it can be introduced gradually. Concepts can be added here and there, experimented with, and seen how it works. It doesn’t require doing groundbreaking changes or differ approach you are having in your codebase. Later, you can apply more advanced constructs elsewhere.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
iteo

iteo

iteo is an international digital product studio founded in Poland, that helps businesses benefit from technology better. Visit us on www.iteo.com