Concepts
Limitations to templates
Templates are extremely powerful allow us to create reusable and extendible code however there is a caveat to this. Take for example our Point
class from Sections 1-2. Point
just takes a single type T
. This can be absolutely any type. This can become a problem though later for the user. Say we have a Point<std::string>
. This will cause an error for the user of our Point
class if they try to take two Point<std::string>
because -
is not supported by std::string
. This can be a major problem as the error produced can be ambiguous to the user and require looking at the source code in depth to diagnose.
/// ... Point implementation
using namespace std::literals;
auto main() -> int
{
auto p1 = Point{ "Hello"s, "Hi"s };
auto p2 = Point{ "Goobye"s, "Bye"s };
auto p3 = p1 - p2;
std::cout << p3 << std::endl;
return 0;
}
The error output (by GCC). As we can see it is verbose and delves into many of the attempts to make it work, and this is for a simple class.
<source>: In instantiation of 'constexpr Point<typename std::common_type<T, U>::type> Point<T>::operator-(const Point<U>&) [with U = std::__cxx11::basic_string<char>; T = std::__cxx11::basic_string<char>; typename std::common_type<T, U>::type = std::__cxx11::basic_string<char>]':
<source>:96:19: required from here
<source>:61:62: error: no match for 'operator-' (operand types are 'std::__cxx11::basic_string<char>' and 'const std::__cxx11::basic_string<char>')
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ~~^~~~~
In file included from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/string:47,
from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/locale_classes.h:40,
from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/ios_base.h:41,
from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/ios:42,
from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/ostream:38,
from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/iostream:39,
from <source>:1:
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:621:5: note: candidate: 'template<class _IteratorL, class _IteratorR> constexpr decltype ((__y.base() - __x.base())) std::operator-(const reverse_iterator<_IteratorL>&, const reverse_iterator<_IteratorR>&)'
621 | operator-(const reverse_iterator<_IteratorL>& __x,
| ^~~~~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:621:5: note: template argument deduction/substitution failed:
<source>:61:62: note: 'std::__cxx11::basic_string<char>' is not derived from 'const std::reverse_iterator<_IteratorL>'
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ~~^~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:1778:5: note: candidate: 'template<class _IteratorL, class _IteratorR> constexpr decltype ((__x.base() - __y.base())) std::operator-(const move_iterator<_IteratorL>&, const move_iterator<_IteratorR>&)'
1778 | operator-(const move_iterator<_IteratorL>& __x,
| ^~~~~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:1778:5: note: template argument deduction/substitution failed:
<source>:61:62: note: 'std::__cxx11::basic_string<char>' is not derived from 'const std::move_iterator<_IteratorL>'
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ~~^~~~~
<source>:61:71: error: no match for 'operator-' (operand types are 'std::__cxx11::basic_string<char>' and 'const std::__cxx11::basic_string<char>')
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ~~^~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:621:5: note: candidate: 'template<class _IteratorL, class _IteratorR> constexpr decltype ((__y.base() - __x.base())) std::operator-(const reverse_iterator<_IteratorL>&, const reverse_iterator<_IteratorR>&)'
621 | operator-(const reverse_iterator<_IteratorL>& __x,
| ^~~~~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:621:5: note: template argument deduction/substitution failed:
<source>:61:71: note: 'std::__cxx11::basic_string<char>' is not derived from 'const std::reverse_iterator<_IteratorL>'
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ~~^~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:1778:5: note: candidate: 'template<class _IteratorL, class _IteratorR> constexpr decltype ((__x.base() - __y.base())) std::operator-(const move_iterator<_IteratorL>&, const move_iterator<_IteratorR>&)'
1778 | operator-(const move_iterator<_IteratorL>& __x,
| ^~~~~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:1778:5: note: template argument deduction/substitution failed:
<source>:61:71: note: 'std::__cxx11::basic_string<char>' is not derived from 'const std::move_iterator<_IteratorL>'
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ~~^~~~~
<source>:61:77: error: no matching function for call to 'Point<std::__cxx11::basic_string<char> >::Point(<brace-enclosed initializer list>)'
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ^
<source>:20:15: note: candidate: 'constexpr Point<T>::Point(Point<T>&&) [with T = std::__cxx11::basic_string<char>]'
20 | constexpr Point(Point&& p) noexcept
| ^~~~~
<source>:20:15: note: candidate expects 1 argument, 2 provided
<source>:16:15: note: candidate: 'constexpr Point<T>::Point(const Point<T>&) [with T = std::__cxx11::basic_string<char>]'
16 | constexpr Point(const Point& p) noexcept
| ^~~~~
<source>:16:15: note: candidate expects 1 argument, 2 provided
<source>:12:5: note: candidate: 'constexpr Point<T>::Point(T, T) [with T = std::__cxx11::basic_string<char>]'
12 | Point(T x, T y) noexcept
| ^~~~~
<source>:12:5: note: conversion of argument 1 would be ill-formed:
<source>:9:15: note: candidate: 'constexpr Point<T>::Point() [with T = std::__cxx11::basic_string<char>]'
9 | constexpr Point() = default;
| ^~~~~
<source>:9:15: note: candidate expects 0 arguments, 2 provided
ASM generation compiler returned: 1
<source>: In instantiation of 'constexpr Point<typename std::common_type<T, U>::type> Point<T>::operator-(const Point<U>&) [with U = std::__cxx11::basic_string<char>; T = std::__cxx11::basic_string<char>; typename std::common_type<T, U>::type = std::__cxx11::basic_string<char>]':
<source>:96:19: required from here
<source>:61:62: error: no match for 'operator-' (operand types are 'std::__cxx11::basic_string<char>' and 'const std::__cxx11::basic_string<char>')
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ~~^~~~~
In file included from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/string:47,
from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/locale_classes.h:40,
from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/ios_base.h:41,
from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/ios:42,
from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/ostream:38,
from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/iostream:39,
from <source>:1:
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:621:5: note: candidate: 'template<class _IteratorL, class _IteratorR> constexpr decltype ((__y.base() - __x.base())) std::operator-(const reverse_iterator<_IteratorL>&, const reverse_iterator<_IteratorR>&)'
621 | operator-(const reverse_iterator<_IteratorL>& __x,
| ^~~~~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:621:5: note: template argument deduction/substitution failed:
<source>:61:62: note: 'std::__cxx11::basic_string<char>' is not derived from 'const std::reverse_iterator<_IteratorL>'
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ~~^~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:1778:5: note: candidate: 'template<class _IteratorL, class _IteratorR> constexpr decltype ((__x.base() - __y.base())) std::operator-(const move_iterator<_IteratorL>&, const move_iterator<_IteratorR>&)'
1778 | operator-(const move_iterator<_IteratorL>& __x,
| ^~~~~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:1778:5: note: template argument deduction/substitution failed:
<source>:61:62: note: 'std::__cxx11::basic_string<char>' is not derived from 'const std::move_iterator<_IteratorL>'
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ~~^~~~~
<source>:61:71: error: no match for 'operator-' (operand types are 'std::__cxx11::basic_string<char>' and 'const std::__cxx11::basic_string<char>')
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ~~^~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:621:5: note: candidate: 'template<class _IteratorL, class _IteratorR> constexpr decltype ((__y.base() - __x.base())) std::operator-(const reverse_iterator<_IteratorL>&, const reverse_iterator<_IteratorR>&)'
621 | operator-(const reverse_iterator<_IteratorL>& __x,
| ^~~~~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:621:5: note: template argument deduction/substitution failed:
<source>:61:71: note: 'std::__cxx11::basic_string<char>' is not derived from 'const std::reverse_iterator<_IteratorL>'
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ~~^~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:1778:5: note: candidate: 'template<class _IteratorL, class _IteratorR> constexpr decltype ((__x.base() - __y.base())) std::operator-(const move_iterator<_IteratorL>&, const move_iterator<_IteratorR>&)'
1778 | operator-(const move_iterator<_IteratorL>& __x,
| ^~~~~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/stl_iterator.h:1778:5: note: template argument deduction/substitution failed:
<source>:61:71: note: 'std::__cxx11::basic_string<char>' is not derived from 'const std::move_iterator<_IteratorL>'
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ~~^~~~~
<source>:61:77: error: no matching function for call to 'Point<std::__cxx11::basic_string<char> >::Point(<brace-enclosed initializer list>)'
61 | { return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
| ^
<source>:20:15: note: candidate: 'constexpr Point<T>::Point(Point<T>&&) [with T = std::__cxx11::basic_string<char>]'
20 | constexpr Point(Point&& p) noexcept
| ^~~~~
<source>:20:15: note: candidate expects 1 argument, 2 provided
<source>:16:15: note: candidate: 'constexpr Point<T>::Point(const Point<T>&) [with T = std::__cxx11::basic_string<char>]'
16 | constexpr Point(const Point& p) noexcept
| ^~~~~
<source>:16:15: note: candidate expects 1 argument, 2 provided
<source>:12:5: note: candidate: 'constexpr Point<T>::Point(T, T) [with T = std::__cxx11::basic_string<char>]'
12 | Point(T x, T y) noexcept
| ^~~~~
<source>:12:5: note: conversion of argument 1 would be ill-formed:
<source>:9:15: note: candidate: 'constexpr Point<T>::Point() [with T = std::__cxx11::basic_string<char>]'
9 | constexpr Point() = default;
| ^~~~~
<source>:9:15: note: candidate expects 0 arguments, 2 provided
Execution build compiler returned: 1
To address this C++20 introduced concepts. A mechanism for imposing constraints on types.
What is a Concept?
A concepts is a set of conditions and requirements imposed on a type that is checked at compile time and evaluates as a Boolean. Before C++20, template metaprogramming and SFINAE where used to statically impose constraints on types but they had limitations and were highly verbose. Concepts allow us to define syntactic constraints on a template type and then impose those constraints on other types. Concepts are introduced using a template declaration followed by a concept declaration (similar to a class declaration but replace the keyword class
with concept
). Concepts can be composed of other concepts using ||
and &&
(holding similar semantics to there Boolean equivalents). It is difficult to create meaningful concepts as one, they are very new to both C++ but also programming in general, instead try and use the concepts defined by the standard library; from the <concepts>
header, first and impose them on a case by case basis using the techniques we are going to learn below.
#include <concepts>
#include <iomanip>
#include <iostream>
#include <string>
#include <utility>
/// Concept defining a type that can be hashed using `std::hash`
template<typename H>
concept Hashable = requires (H a)
{
{ std::hash<H>{}(a) } -> std::convertible_to<std::size_t>;
};
struct NotHashable {};
using namespace std::literals;
auto main() -> int
{
std::cout << std::boolalpha;
std::cout << "Is Hashable<int>: " << Hashable<int> << " : std::hash<int>{}(69) = " << std::hash<int>{}(69) << std::endl;
std::cout << "Is Hashable<float>: " << Hashable<float> << " : std::hash<float>{}(4.5756f) = " << std::hash<float>{}(4.5756f) << std::endl;
std::cout << "Is Hashable<double>: " << Hashable<double> << " : std::hash<double>{}(-0.0036565764) = " << std::hash<double>{}(-0.0036565764) << std::endl;
std::cout << "Is Hashable<std::string>: " << Hashable<std::string> << " : std::hash<std::string>{}() = " << std::hash<std::string>{}(""s) << std::endl;
std::cout << "Is Hashable<NotHashable>: " << Hashable<NotHashable> << std::endl;
return 0;
}
Constrained templates
Concepts are easiest to use when constraining templates type parameters. Instead of the using the keyword typename
when can instead use a concept. This will impose the rules on the template type at the point of instantiation. We can see this best with our Point
class. Being that a 'point' is a numerical value in a field; say coordinates on a cartesian plane we might want to restrict the type parameter T
of Point
to a number type. C++'s concepts library already has a concept for this called std::integral
. Lets impose this new template type parameter constraint on T
.
template<std::integral T>
class Point
{
/// ... implementation
};
using namespace std::literals;
auto main() -> int
{
auto p1 = Point{ "Hello"s, "Hi"s };
return 0;
}
While C++ error messages aren't the most readable as the compiler tries many different things, amongst the errors is one stating 'template constraint failed ...'. This indicates to use that std::string
is not an appropriate type for Point
without needed to try and subtract two Point<std::string>
or any other operation on it. The instantiation failed at construction.
# ... other error info
<source>: In substitution of 'template<class T> Point(T, T)-> Point<T> [with T = std::__cxx11::basic_string<char>]':
<source>:94:37: required from here
<source>:13:5: error: template constraint failure for 'template<class T> requires integral<T> class Point'
<source>:13:5: note: constraints not satisfied
In file included from <source>:1:
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/concepts: In substitution of 'template<class T> requires integral<T> class Point [with T = std::__cxx11::basic_string<char>]':
<source>:13:5: required by substitution of 'template<class T> Point(T, T)-> Point<T> [with T = std::__cxx11::basic_string<char>]'
<source>:94:37: required from here
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/concepts:100:13: required for the satisfaction of 'integral<T>' [with T = std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >]
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/concepts:100:24: note: the expression 'is_integral_v<_Tp> [with _Tp = std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >]' evaluated to 'false'
100 | concept integral = is_integral_v<_Tp>;
# ... Other error info
Requires expressions
The is a slight problem with our reformed Point
class. It no longer can accept floating point types as they are not considered integrals in C++. Instead we need to constraint Point
to a type T
by creating a conjunction (&&
) or disjunction (||
) of multiple concepts. To do this we use a requires clause. Requires clause are introduced just after (often syntactically below) a template declaration and consist of a list of requirements in the form of a Boolean concept expression. We can create a disjunction of the std::integral
and std::floating_point
allowing floating point types and integral types to be valid T
for Point
.
template<typename T>
requires std::integral<T> || std::floating_point<T>
class Point
{
/// ... implementation
};
auto main() -> int
{
auto p1 = Point{ 0.567, 45.657 };
auto p2 = Point{ 1, 9 };
std::cout << p1 + p2 << std::endl;
std::cout << p1 - p2 << std::endl;
return 0;
}
Note: We could specify a more desirable constraint using the
std::is_arithmetic_v
type trait as a concept combines the two ideas better semantically but fine tuningPoint
can be an exercise for you.
Requires expression
Sometimes more complicated requirements need to be specified in order to fine the allowed behaviour of the program. Require expressions allow for mock values of template types to bne created and specify patterns that a value of the type parameter must provide. In the case of our Point
class, we may want to also allow other types that support +
and -
binary operator overloads. Because this check depends on the type parameter of another Point
we can't declare it at the requires clause at the template declaration of Point
. Instead we need to create a requires clause at the operator overloads template declaration. We then declare our requires expression, enforcing the semantic and syntactic rules on the type. We can declare as many of these rules in a requires expression as we like. For Point
we will ensure that for a value of type T
called a
and for a value of type U
of name b
, a + b
and a - b
is valid.
/// ... Point details
template<typename U>
requires requires (T a, U b)
{
a + b;
}
constexpr auto
operator+ (const Point<U>& p)
noexcept -> Point<typename std::common_type<T, U>::type>
{ return Point<typename std::common_type<T, U>::type>{ x + p.x, y + p.y }; }
template<typename U>
requires requires (T a, U b)
{
a - b;
}
constexpr auto
operator- (const Point<U>& p)
noexcept -> Point<typename std::common_type<T, U>::type>
{ return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
/// ... Point details
Note: The double
requires requires
notation is called an ad-hoc constraint. The firstrequires
introduces a requires clause which can have concepts, conjunctions, disjunctions or requires expressions. The secondrequire
introduces a requires expression which takes a similar syntactic form to functions.
Compound requirements
We can also apply constraints on the expressions within a requires expression, ensuring the types or properties these expressions will have. These are called compound expressions, which we create by wrapping our individual rules from a requires expression in braces and use a ->
followed by a constraint on the return type of the expression.
/// ... Point details
template<typename U>
requires requires (T a, U b)
{
{ a + b } -> std::same_as<typename std::common_type<T, U>::type>;
}
constexpr auto
operator+ (const Point<U>& p)
noexcept -> Point<typename std::common_type<T, U>::type>
{ return Point<typename std::common_type<T, U>::type>{ x + p.x, y + p.y }; }
template<typename U>
requires requires (T a, U b)
{
{ a - b } -> std::same_as<typename std::common_type<T, U>::type>;
}
constexpr auto
operator- (const Point<U>& p)
noexcept -> Point<typename std::common_type<T, U>::type>
{ return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }
/// ... Point details
Constrained variables
We can also use concepts to constrain variables declared with the auto
specifier. Gives a more robust option for constraining function and method parameters without the need for template declarations or requires clauses.
#include <concepts>
#include <iostream>
auto println(const std::integral auto& v) -> void
{ std::cout << v << std::endl; }
auto main() -> int
{
println(9);
// println(46.567); ///< fails
return 0;
}
Note: We've applied concepts at the user definition level for classes. These concepts [ideas] can be applied to template functions as well. They are also the basic for defining your own concepts.