In a previous Tech Tip, I presented a brief overview of using type traits in C++; namely, those that can be found in the standard library. These are useful tools for constraining and limiting the types of data that can be used as template arguments to either templated classes, or templated functions/method. As a continuation, in this short edition we’ll examine an extension of these ideas in the form of Concepts. Concepts have been a technique employed in C++ for decades, but with the emergence of C++20, now form an actual part of the language. Concepts are predicates, which the compiler evaluates, to constrain types to those that satisfy certain requirements.
To examine the utility of Concepts, we’ll consider the following simple yet illustrative snippet (from en.cppreference.com/w/cpp/language/constraints):
#include <string>
#include <cstddef>
#include <concepts>
// Declaration of the concept "Hashable", which is satisfied by any type 'T'
// such that for values 'a' of type 'T', the expression std::hash<T>{}(a)
// compiles and its result is convertible to std::size_t
template<typename T>
concept Hashable = requires(T a) {
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
struct meow {};
// Constrained C++20 function template:
template<Hashable T>
void f(T) {}
//
// Alternative ways to apply the same constraint:
// template<typename T>
// requires Hashable<T>
// void f(T) {}
//
// template<typename T>
// void f(T) requires Hashable<T> {}
int main() {
using std::operator""s;
f("abc"s); // OK, std::string satisfies Hashable
//f(meow{}); // Error: meow does not satisfy Hashable
}
In the example, we see a template function f(T), templatized on the parameter type T. You may notice, the familiar syntax from the past which might appear as :
template<typename T>
void f(T)
{ /* Do something with a type T */ }
Has been replaced with the slightly different:
template<Hashable T>
void f(T)
{ /* Do something with a type T that satisfies the Hashable concept */ }
So, unless the applied type satisfies the Hashable Concept (convertible in the prescribed manner, seen on line 10), we will get a useful error message at compile time. To illustrate this, we can alter main() to be:
int main() {
using std::operator""s;
//f(“abs”s);// OK, std::string satisfies Hashable
f(meow{}); // Error: meow does not satisfy Hashable
}
The compiler (VS2019, in this case) now gives the following output:
error C7602: ‘f’: the associated constraints are not satisfied
While this example is quite simple, you can imagine that if used in composition, Concepts can be a powerful tool to enforce the conformity of template arguments to certain properties and features at compile time, while giving simple, coherent compile-time errors if they are violated. Beginning with C++20, these techniques have been formalized as part of the language, increasing accessibility and ease-of-use. Hopefully this has served as a useful, if simple, extension of the earlier discussion on the utility of basic type traits in C++. For a bit more detail, a fuller and more formal treatment can be found at the source of the example (en.cppreference.com/w/cpp/language/constraints).
By Brian Sohr, Principal Software Developer