Template Constraints, introduced with Concepts in C++20, provide a way to impose conditions on template parameters, allowing developers to restrict the types and values that can be used in templates. This feature makes templates more readable, safe, and maintainable by ensuring that template arguments meet specific requirements or concepts.
Before C++20, constraints were typically enforced using techniques like SFINAE (Substitution Failure Is Not An Error), which could make code complex and error messages cryptic. With Concepts, template constraints become first-class citizens in the language, offering a more intuitive way to express template requirements.
A concept is a compile-time predicate (i.e., a condition) that specifies the properties or behaviors a type must satisfy to be used as a template argument. If the type fails to meet the conditions, the template instantiation fails with a clear error message.
template <typename T>
concept ConceptName = /* predicate or condition */;
Concepts can be used to constrain templates in a few different ways:
typename or class)requires clause to add constraints at the end of a template declarationHere’s an example of a concept that checks if a type supports the < operator (i.e., if a type is comparable):
template <typename T>
concept LessThanComparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>; // Must support the '<' operator and return a boolean
};
In this concept:
requires introduces a list of expressions that the type T must satisfy.{ a < b } -> std::convertible_to<bool>; checks that the result of a < b can be converted to a bool.You can use the concept directly in the template parameter list, replacing typename or class:
template <LessThanComparable T> // Constrain 'T' using the 'LessThanComparable' concept
bool is_smaller(const T& a, const T& b) {
return a < b;
}
T is constrained by the LessThanComparable concept, so only types that support the < operator can be passed as arguments to is_smaller.requires Clauses:You can also use requires clauses to apply constraints after the template parameter list:
template <typename T>
requires LessThanComparable<T> // 'requires' clause constraining 'T'
bool is_smaller(const T& a, const T& b) {
return a < b;
}
This is equivalent to the previous example but uses the requires clause for readability.
C++20 allows you to abbreviate the syntax further by directly using the concept name in place of the typename keyword:
bool is_smaller(LessThanComparable auto a, LessThanComparable auto b) {
return a < b;
}
This is the most concise way to express template constraints using Concepts. The keyword auto here is shorthand for “any type that satisfies the given concept.”
C++20 provides several standard library concepts in the <concepts> header. These built-in concepts allow you to impose common requirements on template arguments without defining custom concepts.
Some common standard concepts include:
std::integral: Requires the type to be an integral type (e.g., int, char).std::floating_point: Requires the type to be a floating-point type (e.g., float, double).std::convertible_to<From, To>: Requires that a type can be converted from From to To.std::same_as<T, U>: Requires that T and U are the same type.#include <concepts>
template <std::integral T> // Only integral types are allowed
T gcd(T a, T b) {
while (b != 0) {
T temp = b;
b = a % b;
a = temp;
}
return a;
}
int main() {
gcd(10, 15); // OK, both are integers
// gcd(10.5, 15.2); // Error, because 'double' is not an integral type
}
In this example:
gcd function is constrained by the std::integral concept, ensuring that only integral types (e.g., int, char) can be passed as template arguments.double is passed, the compiler will produce a meaningful error message indicating that the type does not satisfy the std::integral concept.You can create custom concepts with multiple conditions to refine your template requirements. For instance, the following concept checks if a type is both copyable and default-constructible:
#include <concepts>
template <typename T>
concept CopyableAndDefaultConstructible = std::copyable<T> && std::default_initializable<T>;
template <CopyableAndDefaultConstructible T>
T create_and_copy(const T& obj) {
T copy = obj; // Requires copy constructor
return copy; // Requires default constructor
}
In this example:
CopyableAndDefaultConstructible is a concept that checks two conditions: that the type T is both copyable and default-constructible.create_and_copy function can only be instantiated with types that satisfy both of these conditions.You can refine concepts by combining or extending existing ones. For example:
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // Type must support '+' operator
};
template <typename T>
concept AddableAndLessThanComparable = Addable<T> && LessThanComparable<T>; // Combine two concepts
In this example:
Addable checks if a type supports the + operator.AddableAndLessThanComparable combines both the Addable and LessThanComparable concepts to impose more constraints on the type.requires ExpressionsAnother feature introduced with Concepts is the requires expression, which allows you to write inline constraints within a function body.
template <typename T>
void print_if_addable(const T& a, const T& b) {
if constexpr (requires { a + b; }) { // Check if 'a + b' is a valid expression
std::cout << a + b << std::endl;
} else {
std::cout << "Cannot add these types!" << std::endl;
}
}
In this example:
requires expression inside if constexpr checks whether a + b is a valid expression.+ operator, it prints the result; otherwise, it prints an error message.