How to use the <requires> clause with concepts in C++ functions

A concept is a set of restrictions on template parameters evaluated at compile time. In this shot, we will write a concept in a function using the requires clause.

How to write the requires clause

We use the requires clause between the template parameter list and the function return type, which is auto in this case.

#include <iostream>
#include <concepts>
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template <typename T>
requires Number<T>
auto add(T a, T b) {
return a+b;
}
int main() {
std::cout<<add(2,1.2f)<<'\n';
return 0;
}

Note: We use the concept and define our constraint in the requires clause, expecting that any T template parameter must satisfy the concept Number requirements.

Using multiple types

If we want the capability of adding up numbers of multiple types, we’d need to introduce a second template parameter.

template <typename T,
          typename U>
requires Number<T> && Number<U>
auto add(T a, U b) {
  return a+b;
}

Then calls such as add(1, 2.14) will also work. Please note that the concept was modified. The drawback is that you’d need to introduce a new template parameter for each new function parameter and its requirement.

Multiple types of parameters in a template

It gives us some error because both parameters of the add function should be the same type.

If we want to add two numbers of multiple types, we have to change our template. Our template should look like this now:

#include <iostream>
#include <concepts>
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template <typename T, typename U>
requires Number<T> && Number<U>
auto add(T a, U b) {
return a+b;
}
int main() {
std::cout<<add(5,42.1f)<<'\n';
return 0;
}

More constraints

We can also express more complex constraints with the requires clause. For the sake of an example, let’s just inline the definition of Number:

template <typename T>
requires std::integral<T> || std::floating_point<T>
auto add(T a, T b) {
return a+b;
}

For better readability, in most cases, it’s a good idea to name your concepts, especially if you have more complex representations. Take a look at the code below for a full example.

#include <iostream>
#include <concepts>
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template <typename T, typename U>
requires Number<T> && Number<U>
auto add(T a, U b) {
return a+b;
}
template <typename T, typename U>
requires std::integral<T> || std::floating_point<T> &&\
std::integral<U> || std::floating_point<U>
auto add2(T a, U b) {
return a+b;
}
int main() {
std::cout<<add(5,42.1f)<<'\n';
std::cout<<add2(42.1f,5)<<'\n';
return 0;
}

The add() on line 8 is consuming a named concept, Number, using the requires clause. It takes two numbers as a parameter, which should be either integer or floating_point, and returns the sum of both numbers.

On line 16, another function, add2(), is defined, which takes two numbers as parameters and returns the sum, but uses an unnamed concept through the requires clause.

Copyright ©2024 Educative, Inc. All rights reserved