Introduction
A class template, function template (including lambdas) may be associated with a constraint, which specifies requirements on the template arguments. This can be used to select the most appropriate function overload or template specialization.
A concept is a named set of such constraints. A concept is ultimately a logical predicate \(P(x)\), evaluated at compile-time, where \(x\) represents template parameters. A function or class template constrained by the concept \(P\), will work only for template arguments that satisfy \(P\).
Consider the templated function:
#include <iostream>
#include <complex>
template<typename T>
(T const a, T const b){
T sumreturn (a + b);
}
int main()
{
using namespace std::literals::complex_literals;
int x{2}, y{3};
(x, y);
sum(2.71828, 3.14159);
sum(std::complex{1.0 + 1.0i}, std::complex{1.0 - 1.0i});
sum//sum("42", "1"); //Error cannot add two strings
return 0;
}
The sum
function returns the result of applying the binary operator+(T,T)
on its arguments. The sum
function only makes sense when we discuss mathematical types such as integers, floating-point numbers, std::complex<double>
, vectors and matrices. For most types, overloading the operator +
makes no sense at all.
Therefore, just by looking at the declaration of this function, without inspecting its body, we cannot really say what this function may accept as input and what it does.
The intention for our sum
function template is to allow passing only types that support arithmetic operations. One way is to use std::enable_if
:
#include <iostream>
#include <complex>
#include <type_traits>
template<typename T,
typename = typename std::enable_if<std::is_arithmetic_v<T>,T>>
(T const a, T const b){
T sumreturn (a + b);
}
int main()
{
using namespace std::literals::complex_literals;
int x{2}, y{3};
(x, y);
sum(2.71828, 3.14159);
sum(std::complex{1.0 + 1.0i}, std::complex{1.0 - 1.0i});
sum("42", "1");
sum
return 0;
}
We added an anonymous template parameter which calls the type metafunction std::enable_if<C,T>
from the type_traits
library. If the condition C
evaluates to std::true_type
, then std::enable_if<C,T>
returns T
. Since std::is_arithmetic_v<const char*>
returns false_type
, enable_if
meta-function doesn’t return anything and the code will not build.
With this implementation, the code readability has decreased. The second type template parameter is difficult to read and certainly requires good TMP knowledge. The compiler error message is also cryptic.
We can improve these two aspects (code readability and compiler error messages) in C++ 20 by using constraints. These are introduced with the requires
keyword as follows:
#include <iostream>
#include <complex>
#include <type_traits>
#include <concepts>
template<typename T>
requires std::is_arithmetic_v<T>
(T const a, T const b){
T sumreturn (a + b);
}
int main()
{
int x{2}, y{3};
(x, y);
sum(2.71828, 3.14159);
sum("42", "1");
sum
return 0;
}
The compiler error message is more meaningful and states that the constraint is_arithmetic_v<const char*>
evaluates to false
.
The requires
keyword introduces a clause, called the requires clasuse, that defines constraints on the template parameters. A constraint is a predicate that evaluates to true
or false
at compile-time. The expression used in the previous example, std::is_arithmetic_v<T>
is simply using a standard type-trait.
Defining Concepts
Many constraints are generic and can be used in multiple places. For example, the functions below require that the type T
be arithmetic.
/* Add 2 scalars */
template<typename T>
requires std::is_arithmetic_v<T>
operator+(T const v1, T const v2){
T return (v1 + v2);
}
/* Add 2 vectors component-wise */
template<typename T>
requires std::is_arithmetic_v<T>
std::vector<T> operator+(std::vector<T> const v1, std::vector<T> const v2){
std::vector<T> result{v1};
for(int i{0};i<v1.size();++i){
[i] = v1[i] + v2[i];
result}
return result;
}
/* Multiply 2 scalars */
template<typename T>
requires std::is_arithmetic_v<T>
operator*(T const a, T const b){
T return (a * b);
}
/* Scalar multiplication of a vector v*/
template<typename T>
requires std::is_arithmetic_v<T>
std::vector<T> operator*(T k, std::vector<T> v){
std::vector<T> result{v};
std::transform(std::begin(v), std::end(v), std::begin(result), [&](auto element){
return (k * element);
});
return result;
}
To avoid this repetitive code, we can defined a named constraint that can be reused in multiple places. A named constraint is called a concept. A concept is defined with the concept
keyword. Here is an example:
template<typename T>
concept arithmetic = std::is_arithmetic_v<T>;
Even though they are assigned a boolean value, concept names should not contain verbs. They represent requirements and are used as attributes or qualifiers on template parameters. So, prefer names like arithmetic, copyable, serializable, container and not is_arithmetic
, is_copyable
, is_serializable
and is_container
. The arithmetic
concept can be used as follows:
#include<iostream>
#include<type_traits>
#include<concepts>
#include<vector>
template<typename T>
concept arithmetic = std::is_arithmetic_v<T>;
/* Add 2 scalars */
template<arithmetic T>
operator+(T const v1, T const v2){
T return (v1 + v2);
}
/* Add 2 vectors component-wise */
template<arithmetic T>
std::vector<T> operator+(std::vector<T> const v1, std::vector<T> const v2){
std::vector<T> result{v1};
for(int i{0};i<v1.size();++i){
[i] = v1[i] + v2[i];
result}
return result;
}
/* Multiply 2 scalars */
template<arithmetic T>
operator*(T const a, T const b){
T return (a * b);
}
/* Scalar multiplication of a vector v*/
template<arithmetic T>
std::vector<T> operator*(T k, std::vector<T> v){
std::vector<T> result{v};
std::transform(std::begin(v), std::end(v), std::begin(result), [&](auto element){
return (k * element);
});
return result;
}
int main()
{
2 + 3;
std::vector{1.0,2.0} + std::vector{3.0,4.0};
2 * 3;
2.0 * std::vector{1.0,-1.0};
return 0;
}
The arithmetic
concept can also be defined using a requires expression. A requires expression uses curly braces {}
and it contains a sequence of requirements.
Consider the case where we want to define a template that only takes container types as an argument. A container type is not easy to define formally. We can do this based on some properties of standard containers.
They have the member types
value_type
,size_type
,allocator_type
,iterator
andconst_iterator
.They have the member function
size()
that returns the elements of the container.They have the member functions
begin()
,end()
,cbegin()
andcend()
that return iterators and constant iterators to the first and one-past-the-last element in the container.
We can define as is_container
type trait as follows:
#include <type_traits>
template<typename T, typename U = void>
struct is_container : std::false_type {};
template<typename >
struct is_container<T,
std::void_t<typename T::value_type,
typename T::size_type,
typename T::allocator_type,
typename T::iterator,
typename T::const_iterator,
decltype(std::declvalue<T>().size()),
decltype(std::declvalue<T>().begin()),
decltype(std::declvalue<T>().end()),
decltype(std::declvalue<T>().cbegin()),
decltype(std::declvalue<T>().cend())
>> : std::true_type {};
template<typename T, typename U=void>
constexpr bool is_container_v<T,U> = is_container<T>::value;
Note, that std::void_t<Args...>
is a utility type metafunction that maps a sequence of any types to void
. It is a convenient way to leverage SFINAE prior to C++20’s concepts.
Concepts make writing such a template constraint much easier. We can employ the concept syntax and requires expressions to define the following:
template<typename T>
concept container = requires(T cont)
{
typename T::value_type;
typename T::size_type;
typename T::allocator_type;
typename T::iterator;
typename T::const_iterator;
.size();
cont.begin();
cont.end();
cont.cbegin();
cont.cend();
cont};
This definition is both shorter and more readable. It uses both simple requirements such as t.size()
as well as type requirements such as T::value_type
. It can be used to constrain template parameters in the manner seen previously.
requires
expressions
The requires
expression in the body of a concept has a function-like syntax.
requires(parameter-list){
requirement-seq;
}
It is a prvalue expression of type bool
that describes the constraints on some template arguments. Such an expression is true
if the constraints are satisfied and false
otherwise.
The substitution of template arguments into a requires
expression used in the declaration of a templated entity may result in the formation of invalid types or expressions, or the violation of the semantic constraints of the requirements. In such cases, the requires
-expression evaluates to false
and does not cause the program to be ill-formed.
Simple requirements
A simple requirement is an expression that is not evaluated but only checked for correctness. The expression must be valid for the requirement to be evaluated to true
.
template<typename T>
concept arithmetic requires(T a){
std::is_arithmetic_v<T>;
};
Type requirements
Type requirements are introduced with the typename
keyword followed by the name of a type. We can use it verify if :
- A nested type exists(such as in
typename T::value_type
). - A class template specialization names a type.
- An alias template specialization names a type.
Let’s code up a few examples.
template<typename T>
concept KeyValuePair = requires{
typename T::key_type;
typename T::value_type;
}
template<typename T, typename U>
struct Pair{
using key_type = T;
using value_type = U;
key_type key;
value_type value;
};
Pair
satisfies the concept KeyValuePair
, as it has inner types key_type
and value_type
. To verify this is indeed the case, we can use KeyValuePair
as a compile-time metafunction.
static_assert(KeyValuePair<Pair>);
static_assert(!KeyValuePair<std::pair>);
std::pair<T,U>
does have inner types, but they are called first_type
and second_type
.
#include<iostream>
#include<type_traits>
#include<concepts>
template<typename T>
concept arithmetic = std::is_arithmetic_v<T>;
template<arithmetic T>
struct Point2D{
;
T x;
T y};
template<typename T>
using Ref = T&;
template<typename T>
concept C = requires(T t){
typename T::inner; // required nested member name
typename Point2D<T>; // required class template specialization
typename Ref<T>; // required alias template specialization
};
Compound requirements
A compound requirement has the form:
{expression} noexcept -> return_type_requirement
and asserts the properties of the named expression. Both the noexcept
and the return_type_requirement
are optional.
Let’s code up a couple of examples.
In the below example, we define a NonThrowing
to check if a function is marked with the noexcept
specifier.
#include<iostream>
#include<type_traits>
#include<concepts>
/*
Template Metaprogramming
Mariusz Bancila
*/
template<typename T>
void f(T) noexcept {}
template<typename T>
void g(T) {}
template <typename F, typename... T>
concept NonThrowing = requires(F&& func, T... t){
{func(t...)} noexcept;
};
template<typename F, typename... T>
requires NonThrowing<F,T...>
void invoke(F&& func, T... t)
{
(t...);
func}
int main()
{
(f<double>, 100.0);
invoke// invoke(g<double>, 100.0); //Error
return 0;
}
The call invoke(g<double>,100.0)
is not valid, because g<double>
may throw an exception, which results in NonThrowing<F,T...>
to evaluating as false
.
Nested requirements
A nested requirement has the form:
requires constraint_expression;
It is introduced by the requires
keyword. Suppose we want to define a function that performs addition on a variable number of arguments. However, we want to impose some conditions:
- There is more than one argument.
- All arguments have the same type.
- The expression
arg1 + arg2 + ... + argn
is valid.
We define a concept called HomogenousRange
as follows:
/*
Template Metaprogramming
Mariusz Bancila
*/
template<typename T, typename... Ts>
inline constexpr bool are_same_v =
std::conjunction_v<std::is_same<T,Ts>...>;
template <typename... T>
concept HomogenousRange = requires(T... t)
{
(... + t);
requires are_same_v<T...>;
requires sizeof...(T) > 1;
}
This concept contains one simple requirement and two nested requirements. std::conjunction_v<B1,...,BN>
is a type metafunction that forms the logical conjunction of conditions B1
,…,BN
, effectively performing a logical AND on the sequence. It works as follows:
- If
sizeof...(B)==0
,std::true_type
otherwise - The first type
Bi
inB1,...,BN
for whichBi
isfalse
orBN
if there is no such type.
The pattern std::is_same<T,Ts>...
is expanded as
std::is_same<T,T1>,std::is_same<T,T2>,...,std::is_same<T,Tn>
Akin to the logical AND operation, if all of them evaluate to std::true_type
, the type metafunction std::conjunction_v<B1,...,Bn
returns std::true_type
.
The simple requirement (... + t)
specifies that left fold expression (adding all the arguments) is a valid operation.
Using this concept, we can define the variadic function template:
#include<iostream>
#include<type_traits>
#include<concepts>
/*
Template Metaprogramming
Mariusz Bancila
*/
template<typename T, typename... Ts>
inline constexpr bool are_same_v =
std::conjunction_v<std::is_same<T,Ts>...>;
template <typename... T>
concept HomogenousRange = requires(T... t)
{
(... + t);
requires are_same_v<T...>;
requires sizeof...(T) > 1;
};
template<typename... T>
requires HomogenousRange<T...>
std::common_type_t<T...> sum(T&&... args){
return (... + args);
}
int main()
{
auto result = sum(1, 2, 3, 4, 5);
return 0;
}
Composing constraints
Constraints can be composed using &&
and ||
operators. A composition of two constraints using the &&
operator is called a conjunction and the composition of two constraints using the ||
operator is called a disjunction.
For a conjunction to be true, both constraints must be true. For a disjunction to be true, atleast one of the constraints must be true.
template<typename T, typename U>
concept signed_integral = std::integral<T> && std::is_signed_v<T>;
template<typename T>
requires std::is_integral_v<T> || std::is_floating_point_v<T>
(T a, T b){
T addreturn (a + b)
}
The standard concepts library
The standard library provides a set of fundamental concepts that can be used to define requirements on template arguments, class templates, variable templates and aliast templates. The standard concepts in C++20 are spread across several headers and namespaces. The main set of concepts is in the <concepts>
header and the std
namespace. Most of the concepts are equivalent to one or more existing type-traits.