Concepts finally appeared in the language with the completion of C++20—I say finally as work to specify them had been going on continuously since before the release of C++11. This article attempts to explain their rationale and usage, with examples that should compile on any up-to-date C++ compiler when specifying -std=c++20 (or /std:c++20 for MSVC).
So why do we need concepts when template syntax has been available since C++98? Put simply, templates have been (successfully) used in ways that were never envisaged; however the reporting of invalid instantiations and similar errors has not kept pace. Also, the use of auto in function parameter lists gives us template syntax “invisibly”, which the novice/intermediate coder may not expect.
Consider the following function:
auto remainder(auto a, auto b) {
return a % b;
}
There are no surprises when calling this function with two positive ints, however calling it with two doubles results in unwanted type coercion (at least in the compilers I tried), and calling with string literals produces a compile-time error. The following code traps both of these latter cases:
#include <iostream>
#include <concepts>
#include <stdexcept>
auto remainder(std::integral auto a, std::integral auto b) {
return a % b;
}
auto remainder(auto a, auto b) {
throw std::runtime_error("Bad types to remainder()");
return -99;
}
The entity std::integral is a C++ concept, defined in header <concepts>, for a full list see this page.
In the above code we’ve jumped straight into the most modern syntax, but concepts can be usefully employed with “traditional” template declarations, too. Say we want to create numbers at compile-time which are always both odd and the product of two positive numbers (from rules of Math these will also be odd). We need a user-defined concept counting_odd, which we will use with the requires keyword like this:
template<typename T, T N, T M>
requires counting_odd<T, N> && counting_odd<T, M>
constexpr T odd_multiply() {
return N * M;
}
int main() {
std::cout << "3 times 5 is an odd number: " << odd_multiply<unsigned, 3, 5>() << '\n';
std::cout << "7 times 13 is an odd number: " << odd_multiply<int, 7, 13>() << '\n';
}
Starting from the other end, let’s create an old-skool style recursive template that determines whether a number is odd or even:
template<long long N>
struct is_odd {
static const bool value = is_odd<N-2>::value;
};
template<long long N>
constexpr bool is_odd_v = is_odd<N>::value;
template<>
struct is_odd<1> {
static const bool value = true;
};
template<>
struct is_odd<0> {
static const bool value = false;
};
static_assert(is_odd_v<3> == true);
static_assert(is_odd_v<4> == false);
This code evaluates recursively, subtracting two each time, until the base case of zero or one is reached, indicating that the original number was even or odd, respectively.
We also need a constexpr function to determine whether the original number was positive, in order to avoid effectively-infinite recursion:
template<typename T, T N>
constexpr bool is_positive() {
return N > T{};
}
static_assert(is_positive<int,3>());
static_assert(!is_positive<unsigned,0>());
static_assert(!is_positive<int,-1>());
All that is still needed is the concept definition before the main code, which is as follows (the <concepts> header is not needed in this case, but <type_traits> is):
template<typename T, T N>
concept counting_odd =
std::is_integral_v<T>
&& is_positive<T,N>()
&& is_odd_v<N>;
Note that the order of these tests is significant as the && will short-circuit. Running the program produces the output (note: static_assert() could also be used):
3 times 5 is an odd number: 15
7 times 13 is an odd number: 91
Remember that the concept keyword defines an entity which evaluates to a boolean at compile-time, and a false value will halt compilation when used with the requires keyword, or prevent instantiation when used with auto parameters. That’s all for this article, the complete source code is available on GitHub.