C++ - C++20 Concepts and Constraints
C++20 introduced Concepts, a major improvement to the C++ template system. Concepts allow programmers to specify what kind of types a template can accept. Before C++20, templates could accept almost any type, and if an unsuitable type was provided, the compiler often produced very long and difficult-to-understand error messages. Concepts solve this problem by allowing developers to define clear requirements for template parameters.
1. What Are Concepts?
A concept is a named set of requirements that a type must satisfy to be used with a template.
For example, suppose we want to create a function that works only with integer-like types. Instead of allowing every possible type and discovering problems during compilation, we can define a requirement explicitly.
#include <concepts>
#include <iostream>
template <std::integral T>
void display(T value)
{
std::cout << value << std::endl;
}
int main()
{
display(10);
display(25);
}
Here, std::integral is a standard C++20 concept. It specifies that T must be an integral type.
The following would not satisfy the requirement:
display(10.5);
because double is not an integral type.
Concepts therefore make templates more precise and easier to understand.
2. Why Were Concepts Introduced?
Before C++20, programmers commonly used templates without explicitly stating their requirements.
For example:
template <typename T>
T add(T a, T b)
{
return a + b;
}
This appears simple, but the compiler does not know beforehand that T is expected to support the + operator.
If a programmer calls:
add(10, 20);
the function works.
But if an incompatible type is supplied, the compiler may generate complicated template-related error messages.
Concepts allow us to express the requirement directly:
template <typename T>
requires std::integral<T>
T add(T a, T b)
{
return a + b;
}
Now the intention is much clearer: this function is intended for integral types.
3. Standard Library Concepts
C++20 provides several predefined concepts in the standard library through the <concepts> header.
Some important examples include:
std::integral
Checks whether a type is an integer type.
std::integral<int>
std::integral<long>
These satisfy the concept.
std::floating_point
Checks whether a type is a floating-point type.
std::floating_point<float>
std::floating_point<double>
std::signed_integral
Checks whether a type is a signed integer.
std::signed_integral<int>
std::unsigned_integral
Checks whether a type is an unsigned integer.
std::unsigned_integral<unsigned int>
std::same_as
Checks whether two types are exactly the same.
std::same_as<int, int>
std::derived_from
Checks whether one type is derived from another type.
These predefined concepts reduce the need to create common type requirements manually.
4. Creating Your Own Concept
One of the most useful features of C++20 is the ability to create custom concepts.
Consider a program where we want a type to support the + operator.
template <typename T>
concept Addable = requires(T a, T b)
{
a + b;
};
The Addable concept requires that two objects of type T can be added.
We can then use it with a function:
template <Addable T>
T add(T a, T b)
{
return a + b;
}
Now the function clearly communicates its requirement.
5. The requires Expression
The requires expression is an important part of C++20 Concepts.
It allows programmers to describe expressions and operations that a type must support.
For example:
template <typename T>
concept Printable = requires(T value)
{
std::cout << value;
};
This concept checks whether a value of type T can be sent to std::cout.
A function can then use this concept:
template <Printable T>
void print(T value)
{
std::cout << value << std::endl;
}
The requires expression does not execute the expression. Instead, it checks whether the expression is valid at compile time.
6. Using Concepts With Template Functions
There are several ways to apply a concept.
The first method places the concept directly after the template keyword:
template <std::integral T>
T square(T value)
{
return value * value;
}
Another method uses the requires clause:
template <typename T>
requires std::integral<T>
T square(T value)
{
return value * value;
}
Both approaches express essentially the same restriction.
A third style places the constraint after the function declaration:
template <typename T>
T square(T value)
requires std::integral<T>
{
return value * value;
}
The first form is often the most concise when using a single concept.
7. Multiple Constraints
A template can have multiple requirements.
For example:
template <typename T>
requires std::integral<T> && std::signed_integral<T>
void process(T value)
{
std::cout << value;
}
Here, T must satisfy both requirements.
Concepts can therefore express more precise conditions than simply specifying a general template type.
8. Combining Concepts
Custom concepts can also be combined.
For example:
template <typename T>
concept Numeric =
std::integral<T> || std::floating_point<T>;
Now Numeric represents either an integral or floating-point type.
We can use it as follows:
template <Numeric T>
T multiply(T a, T b)
{
return a * b;
}
This function can accept integers and floating-point values while excluding unrelated types.
9. Concepts With Classes
Concepts are not limited to functions. They can also constrain class templates.
For example:
template <std::integral T>
class Number
{
private:
T value;
public:
Number(T v) : value(v) {}
T getValue() const
{
return value;
}
};
Now the class can only be instantiated with an integral type.
Number<int> a(100);
Number<long> b(500);
A floating-point instantiation would not satisfy the constraint.
10. Concepts and Template Overloading
Concepts can be particularly useful when different implementations are needed for different types.
For example:
template <std::integral T>
void process(T value)
{
std::cout << "Integer value";
}
template <std::floating_point T>
void process(T value)
{
std::cout << "Floating-point value";
}
When calling:
process(10);
the integral version is selected.
When calling:
process(10.5);
the floating-point version is selected.
This provides a clean way to control template overloads.
11. Concepts Versus Traditional SFINAE
Before C++20, SFINAE, or "Substitution Failure Is Not An Error," was widely used to constrain templates.
A simplified example might involve:
std::enable_if
Such code can become difficult to read:
template <typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T value)
{
std::cout << value;
}
The equivalent C++20 approach is much clearer:
template <std::integral T>
void process(T value)
{
std::cout << value;
}
Concepts make the programmer's intention much easier to understand.
12. Concepts Versus Type Traits
C++ has long provided type traits, such as:
std::is_integral_v<T>
std::is_pointer_v<T>
std::is_same_v<T, U>
Type traits are still useful, but concepts provide a more direct way to express constraints.
For example:
template <typename T>
requires std::is_integral_v<T>
void process(T value)
{
}
can be expressed more naturally as:
template <std::integral T>
void process(T value)
{
}
Concepts therefore improve the readability of template interfaces.
13. Benefits of Concepts
Improved readability
A concept clearly communicates what a template expects.
template <std::integral T>
is immediately understandable.
Better compiler diagnostics
When an invalid type is supplied, the compiler can explain that the type does not satisfy the required concept instead of producing a long chain of template errors.
Better type safety
Concepts prevent inappropriate types from being accepted by templates.
Easier maintenance
Developers can understand template requirements without examining complicated implementation details.
Better template overloading
Different implementations can be selected according to type capabilities.
Reusable requirements
A custom concept can be defined once and reused throughout a project.
14. Practical Example
Consider a generic function that calculates the larger of two values.
Without a concept:
template <typename T>
T larger(T a, T b)
{
return a > b ? a : b;
}
This assumes that T supports the > operator.
With a custom concept:
template <typename T>
concept Comparable = requires(T a, T b)
{
a > b;
};
template <Comparable T>
T larger(T a, T b)
{
return a > b ? a : b;
}
The relationship between the template and its requirement is now explicit.
The concept can also be reused by other functions that require the same comparison capability.
15. Concepts and Generic Programming
Concepts are especially important in generic programming, where algorithms are designed to work with different types.
For example, an algorithm may require a type to:
-
support comparison;
-
support arithmetic;
-
be movable;
-
be copyable;
-
provide a particular member function;
-
provide a particular return type.
Instead of relying on assumptions, these requirements can be expressed directly using concepts.
This makes generic code more predictable and easier to use.
16. Concepts in Modern C++
Concepts are an important part of modern C++ because they improve the relationship between generic programming and type safety.
They do not replace templates. Instead, they provide a mechanism for describing the requirements placed on template parameters.
The general idea can be summarized as:
Template
|
v
What type is accepted?
|
v
Concept
|
v
Does the type satisfy the requirements?
|
+---- Yes ----> Template can be used
|
+---- No -----> Compilation fails with a constraint violation
Conclusion
C++20 Concepts and Constraints provide a cleaner and more powerful way to control template usage. A concept defines the requirements that a type must satisfy, while constraints determine whether a template is eligible for use.
The introduction of concepts makes generic C++ code more readable, safer, and easier to maintain. They also provide better compiler diagnostics and reduce the need for complicated techniques such as SFINAE and extensive type-trait expressions.
For anyone learning modern C++, understanding concept, requires, standard concepts such as std::integral and std::floating_point, and custom requirements using requires expressions is essential for writing clean and robust generic programs.