C++: Let's get comfortable with concepts

HeliumHydride1 pts0 comments

Let's get comfortable with concepts | Dimitris Platis

Dimitris Platis

Software Engineer & Maker

Twitter<br>LinkedIn<br>GitHub<br>Youtube<br>Email

Constraints and concepts are a major feature introduced in C++20, long-awaited by the C++ community.<br>They provide a way to “constrain” templates to make them more expressive, harder to misuse and easier to debug.<br>Additionally, we can use constraints and concepts instead of SFINAE to enable or disable parts of the API based on the properties of the types used as template parameters.

We are about to get comfortable with concepts, so prepare for some “strange” syntax and many practical examples!

Overall, in this tutorial we will explain how to constrain the template types that may be used.<br>In other words, how to introduce compile-time requirements for our class and function templates.<br>With this in mind, one may ask: “Why do we need to make things stricter?”

We will soon see that it’s all about expressing our intent in a rather simple way,<br>without resorting to complex template metaprogramming techniques.

Disclaimer for the language lawyers : No attempt to optimize the code has been made, the focus is on trying to<br>convey concepts in a clear way without distractions. You have been warned. ⚠️

This is a long tutorial, so here is a table of contents:

The “problem” with unconstrained templates

Let’s constrain things a bit

Making our first concept

Focusing on readability

concept vs requires

How do you requires?

requires without curly braces

requires with curly braces

Recap: With VS without curly braces

“Interfaces” for our template types

API for template types?

“Interfaces” for template types without concepts: SFINAE

“Interfaces” for template types with concepts

requires as a “contract”

requires requiring…

More requires

Choosing the right candidate (à la SFINAE)

Specializing member functions

if constexpr and requires

(Avoid) Concepts that are always satisfied

static_assert and concepts

requires { requires }

requires requires { statements...; }

auto function arguments

Concepts with multiple types

1st argument deduction

Concepts with generic lambdas (no template parameter list)

Concepts with generic lambdas (with template parameter list)

Concepts with variadic templates

Concepts with variadic templates and if constexpr

Concepts with variadic templates and requires

Takeaways

The “problem” with unconstrained templates

Before we delve into the “advanced” template metaprogramming things you can do with concepts,<br>let’s first focus on the basics: Expressiveness and readability.<br>We start by analyzing why we need concepts, by taking a look at the following snippet:

template typename Camera><br>class AutonomousCar {<br>Camera mCamera;<br>public:<br>// ... A lot of code<br>};

Assuming we need to implement a Camera for AutonomousCar, we must ensure that Camera<br>has all the necessary member functions and types that AutonomousCar relies on.<br>If we don’t, the program will not compile.

The “problem” here is that reading AutonomousCar to figure out what Camera’s interface should look like<br>might be difficult if AutonomousCar is large and complicated.<br>To give you a better grasp of what I am trying to convey,<br>imagine if we were not using templates and instead had something like this:

#include "Camera.h"

class AutonomousCar {<br>Camera mCamera;<br>public:<br>// ... A lot of code<br>};

Were we not using templates and AutonomousCar and Camera were “normal” classes,<br>it would be clear what Camera should implement by looking at the relevant interface and documentation.<br>With templates “the interface” is not as clear and the compiler errors are not always helpful or easy to understand.

The problem with expressiveness and readability of template types goes, in my opinion, beyond getting something<br>to compile. It is also about communicating the intent of the code to the reader in a clear and foolproof way:

template typename T><br>T getMedianNumber(std::vectorT> values) {<br>std::sort(values.begin(), values.end());<br>return values[values.size() / 2];

What types make sense as T above?

int, double and other numeric types, right? This is mostly due to the, perhaps not ideal, name of the function.

What types can actually be used as T?

Any type that can be compared with and can be copied.<br>In other words a std::string would compile, however it is most likely not the intended use case.<br>One way forward is to make getMedianNumber more explicit:

templatetypename T><br>T getMedianNumber(std::vectorT> values) {<br>static_assert(std::is_integral_vT> || std::is_floating_point_vT>,<br>"T must be an integral or floating-point");<br>std::sort(values.begin(), values.end());<br>return values[values.size() / 2];

This is somewhat better in the sense that compilation will fail if we try to use<br>getMedianNumber with a type that is not intended to be used. That being said, it is somewhat not ideal<br>as one would need to read beneath the function signature to understand the requirements for T.

Let’s constrain things a bit

With C++20 we...

concepts template requires types templates values

Related Articles