Concepts
Available since C++20
Brief:
- Concepts in C++ are named Boolean predicates on template parameters, evaluated at compile time.
- They act as constraints, limiting the set of accepted template arguments for templates such as classes, functions, and variables.
Example:
Say you are writing a templated function, but one that can only operate on integer values - be that int
, uint8_t
, int16_t
, etc.
On previous versions, like C++11, you could write it like this:
#include <type_traits>
#include <iostream>
<template typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
print_integral(T value) {
std::cout << "Integral value: " << value << "\n";
}
int main() {
print_integral(42); // Works, as int is an integral type
print_integral(3.14); // Compilation error, double is not an integral type
return 0;
}
Utilizing std::enable_if
allows the compiler to apply the "Substitution Failure Is Not An Error" rule, or SFINAE, and removes unsupported types for templated functions and classes. This is much safer than possibly encountering an unsupported type during runtime.
Similarly, static_assert
can be used to accomplish the same thing:
<template typename T>
void print_integral(T value) {
static_assert(std::is_integral<T>::value, "T must be an integral type");
std::cout << "Integral value: " << value << "\n";
}
Like how std::is_integral
is used to verify that the templated type is an integer type, std::is_floating_point
can be used to check if a value is a double
or a float
. For any generic numeric type, std::is_numeric
can be used.
Using concepts, this can be simplified to:
#include <concepts>
#include <iostream>
template <std::integral T>
void print_integral(T value) {
std::cout << "Integral value: " << value << "\n";
}
std::integral
is a pre-defined concept that boils down to:
template<class T>
concept integral = std::is_integral_v<T>
But now say you wanted to write a generic templated print function. Before printing, the type of the input needs to be checked to ensure it can be printed. For example, printing int n = 42;
should work, and so should std::string s = "foo";
but perhaps printing auto foo = Foo();
should not.
Therefore, the function inputs need to be sanitized, and the safest way to do that is at compile time.
First, there needs to be a method to determine if a type is a member of a supported list, similar to std::is_integral
and std::is_floating_point
:
#include <type_traits>
template<typename T, typename... U>
struct IsAnyOf : std::disjunction<std::is_same<T, U>...> {};
If using C++17, the std::disjunction
type trait can be utilized. This takes a parameter pack of boolean type traits and returns true if any of the traits are true.
Combined with std::is_same<U, V>
, which is a type trait that evaluates to std::true_type
if U and V are the same type, std::disjunction<std::true_type>
will evaluate to true
.
bool foo = IsAnyOf<int, float, double, int>::value; // foo == true
bool bar = IsAnyOf<char, float, double, int>::value; // bar == false
If using C++11, std::disjunction
is not available, and instead recursive inheritance can be used via std::integral_constant
to check if a type is present in a list of types.
#include <type_traits>
// Base case: No types left to check, default to false
template<typename T, typename...>
struct IsAnyOf : std::false_type {};
// Recursive case: Check if T matches the first type U, or continue with the rest
template<typename T, typename U, typename... Rest>
struct IsAnyOf<T, U, Rest...>
: std::integral_constant<bool, std::is_same<T, U>::value || IsAnyOf<T, Rest...>::value> {};
Now the method to actually check the type of the templated value can be assembled using similar logic.
In C++17:
template<typename T>
struct IsPrintable : std::disjunction<
std::is_integral<T>,
std::is_floating_point<T>,
IsAnyOf<std::remove_cv_t<std::remove_pointer_t<std::decay_t<T>>>, char, wchar_t>
> {};
There are a couple of type trait transformations used here on T
:
- First,
T
is decayed withstd::decay_t
, which removes references and applies array-to-pointer and function-to-pointer conversions. - Next,
std::remove_pointer_t
is applied, which removes a pointer fromT
and returns the underlying type ofT
. - Finally,
std::remove_cv_t
is used to remove andconst
orvolatile
modifiers fromT
.
For C++11:
template<typename T>
struct IsPrintable : std::integral_constant<bool,
std::is_integral<T>::value ||
std::is_floating_point<T>::value ||
IsAnyOf<
typename std::remove_cv<
typename std::remove_pointer<
typename std::decay<T>::type>::type>::type,
char, wchar_t>::value> {};
Like before, a templated struct is implemented by utilizing a std::integral_constant
. Since C++11 doesn't have access to the type trait transformations, they have to be made directly.
For this example, the only types that are considered "printable" are:
- All numeric types, including integral and floating point values. This covers all
int
's,float
, anddouble
. - Any type that can be reduced to either a character array (
char
) or a wide character array (wchar_t
). This includesstd::string
and other string types.
And a function that can safely print function with compile-time type-checking can be written like:
template<typename... Args>
typename std::enable_if<(IsPrintable<Args>::value && ...), void>::type
print_line(const Args&... arguments) {
(std::wcout << ... << arguments) << '\n';
}
Now, we can safely print our type-checked generic variables:
class Foo {};
int main() {
print_line("Example: ", 3.14, " : ", 42, " : [", 'a', L'-', L"Z]"); // Works
print_line("Example: ", 3.14, " : ", Foo(), " : [", 'a', L'-', L"Z]"); // Compilation error, Foo is not printable
}
That sure is a lot just to have some compile-time type-safety checks for templated functions. The good news is that concepts can simplify that whole process down into:
#include <concepts>
#include <iostream>
template<typename T, typename ... U>
concept IsAnyOf = (std::same_as<T, U> || ...);
template<typename T>
concept IsPrintable = std::integral<T> || std::floating_point<T> ||
IsAnyOf<std::remove_cvref_t<std::remove_pointer_t<std::decay_t<T>>>, char, wchar_t>
void print_line(IsPrintable auto const ... arguments) {
(std::wcout << ... << arguments) << '\n';
}
Like above, IsPrintable
is a type trait that includes numeric and character types that std::wcout
can handle.