Concepts in C++ are a feature introduced in C++20 that provide a way to specify constraints on template parameters. They allow you to define requirements or expectations for types used in templates, making your code more expressive and easier to debug by ensuring that templates are only instantiated with types that meet specific criteria.
Concepts make templates safer and more readable by allowing you to specify conditions that types must satisfy at compile time, rather than relying on obscure compiler errors if a type doesn’t meet the template’s requirements.
A concept is a predicate that defines a set of requirements (constraints) for a type or template parameter. You can define a concept using the requires clause, which specifies the operations and properties that a type must have. For example:
#include <iostream> #include <type_traits> // Define a concept that checks if a type is integral template <typename T> concept Integral = std::is_integral_v<T>;
The concept definition here checks to see if the template type T is an integral type (like int, char, etc.), using the std::is_integral type trait. Once it is defined we can use it two write a template function that only accepts integral types.
#include <iostream> #include <iomanip> #include <type_traits> using namespace std; // Define a concept for integral types template <typename T> concept Integral = std::is_integral_v<T>; // A function template that only accepts integral types template <Integral T> T mult(T a, T b) { return a * b; } int main() { cout << mult(5, 10) << endl; // Works: int is integral // cout << mult(3.14, 1.618) << endl; // Error: double is not integral return 0; }
Here the add()
function is constrained to only allow integral type for its template parameter by the Integral
concept. If you try to pass non-integral values, like doubles, the compiler will not match the template and generate an error.
There is an alternate syntax that uses the requires
keyword to constrain template parameters.
#include <iostream> #include <iomanip> #include <type_traits> using namespace std; // Define a concept for integral types template <typename T> concept Integral = std::is_integral_v<T>; // A function template that only accepts integral types template <typename T> requires Integral<T> T mult(T a, T b) { return a * b; } int main() { cout << mult(5, 10) << endl; // Works: int is integral // cout << mult(3.14, 1.618) << endl; // Error: double is not integral return 0; }
The requires syntax allows more flexibility to create complex restrictions on template parameters.
Built-in Concepts in C++20
C++20 includes several built-in concepts for commonly used type checks, such as:
std::integral: Checks if a type is an integral type (like int, char, bool, etc.).
std::floating_point: Checks if a type is a floating-point type (float, double, long double).
std::signed_integral: Checks if a type is a signed integral type.
std::unsigned_integral: Checks if a type is an unsigned integral type.
std::convertible_to: Checks if one type can be converted to another.
std::same_as: Checks if two types are the same.
d#include <iostream> #include <iomanip> #include <type_traits> using namespace std; // A function template that only accepts floating point types template <std::floating_point T> T fp_mult(T a, T b) { return a * b; } // A function template that only accepts types that can be converted to a double template <std::convertible_to<double> T> T double_mult(T a, T b) { return a * b; } // A function template that only accepts chars template <std::same_as<char> T> std::string char_cat(T a, T b) { return std::string(1, a) + std::string(1, b); } int main() { // cout << fp_mult(5, 10) << endl; // Error: int is not floating point cout << fp_mult(3.14, 1.618) << endl; // Works: double is floating point // cout << double_mult("5", "10") << endl; // Error: no conversion to floating point cout << double_mult(3.14F, 1.618F) << endl; // Works: float is convertible to double // cout << char_cat(65, 66) << endl; // Error: ints passed cout << char_cat('a', 'b') << endl; // Works: chars passed return 0; }
In this example we used the standard concepts convertible_to<>
to constrain a template parameter to types that have conversions to double defined. We also used the standard concept same_as<>
to constraint a template parameter to a specific type only. The next example has a number of more complex concepts. See if you can figure out what each one is doing.
#include <iostream> #include <iomanip> #include <type_traits> using namespace std; // Concept that checks if two types can be added using operator+ template <typename T> concept Addable = requires(T a, T b) { { a + b } -> std::convertible_to<T>; }; // Concept that checks if a type supports == and + template <typename T> concept AddableAndSubtractable = requires(T a, T b) { { a - b } -> std::convertible_to<bool>; { a + b } -> std::same_as<T>; }; // Concept that checks if a type supports both ++ and -- template <typename T> concept IncrementableAndDecrementable = requires(T t) { { ++t } -> std::same_as<T&>; // Prefix increment { t++ } -> std::same_as<T>; // Postfix increment { --t } -> std::same_as<T&>; // Prefix decrement { t-- } -> std::same_as<T>; // Postfix decrement }; // Concept that checks if a type supports indexing with [] template <typename T> concept Indexable = requires(T t, std::size_t i) { { t[i] } -> std::convertible_to<typename T::value_type>; }; // Concept for detecting arrays template <typename T> concept ArrayLike = std::is_array_v<T> || std::is_pointer_v<T>; // Concept that checks if a type behaves like a container template <typename T> concept ContainerLike = requires(T t) { t.begin(); t.end(); t.size(); }; // Concept that checks if a type is callable with specific arguments template <typename F, typename... Args> concept CallableWith = requires(F f, Args... args) { { f(args...) } -> std::convertible_to<void>; }; template <Addable T> T add(T a, T b) { return a + b; } template <AddableAndSubtractable T> T addOrSub(T a, T b) { if (b ^ 0x1) { return a - b; } else { return a + b; } } template <IncrementableAndDecrementable T> T incr(T a) { return ++a; } template <IncrementableAndDecrementable T> T decr(T a) { return --a; } template <Indexable T> typename T::value_type getFirst(T& t) { return t[0]; } // Template function that works for both arrays and containers template <typename T> requires ContainerLike<T> void printElements(const T& container) { std::cout << "Container elements: "; for (const auto& elem : container) { std::cout << elem << " "; } std::cout << std::endl; } template <typename T> requires ArrayLike<T*> void printElements(const T* arr, std::size_t size) { std::cout << "Array elements: "; for (std::size_t i = 0; i < size; ++i) { std::cout << arr[i] << " "; } std::cout << std::endl; } // Function template that requires a callable object template <CallableWith<int, int> F> void invokeWithInts(F func) { func(5, 10); // Invoke with two integers } int main() { int a = 5; int b = 10; int arr[] = {1, 2, 3, 4, 5}; std::vector<int> vec = {10, 20, 30, 40}; // A lambda that takes two integers auto myLambda = [](int x, int y) { std::cout << "Lambda called with: " << x << ", " << y << std::endl; }; // cout << add(&a, &b) << endl; // Error: pointers are not addable cout << add(a, b) << endl; // Works: int is addable // cout << addOrSub(&a, &b) << endl; // Error: pointers are not addable/subtractable cout << addOrSub(a, b) << endl; // Works: int is addable and subtractable cout << incr('a') << endl; // Works: char is incrementable cout << decr(3.14) << endl; // Works: double is decrementable cout << incr(&a) << endl; // Works: pointers are incrementable // cout << getFirst(arr) << endl; // Error: array ref is not indexable cout << getFirst(vec) << endl; // Works: pointer is indexable printElements(arr, 5); // Works: array is array-like printElements(vec); // Works: vector is container-like invokeWithInts(myLambda); // Works: lambda is callable with two ints return 0; }
In most of these concept definitions we use the syntax { expression; } -> constraint
which specifies an expression that must compile with the template type followed by a constraint on the resulting expression.