C++ from Basics to Mastery — Part 16
Express template requirements with concepts and requires
The articles on this blog are written for teaching purposes. The information may contain mistakes, so verify it against your target compiler and standard library.
Templates apply one rule to many types, but an unsupported type can produce a long error deep inside an implementation. C++20 concepts and requires expressions move those requirements into the template's public declaration.
The problem with an unconstrained template
template <typename T>
int GetHealth(const T& value)
{
return value.Health();
}
Only types with a callable Health() member can use this function, yet the signature does not say so. The compiler discovers the problem only after it instantiates the body.
Naming requirements with a concept
#include <concepts>
template <typename T>
concept Damageable = requires(T value, int amount)
{
{ value.Health() } -> std::convertible_to<int>;
{ value.ApplyDamage(amount) } -> std::same_as<void>;
};
template <Damageable T>
void Attack(T& target, int damage)
{
target.ApplyDamage(damage);
}
A requires expression checks whether syntax and result types are valid without executing the operations. The contract of Attack is now visible before its body, and invalid calls fail closer to the call site.
Constraints can include type properties
template <typename T>
concept SmallId = std::integral<T> && (sizeof(T) <= 4);
template <SmallId T>
void WriteId(T id);
Concepts combine type traits and requirements with logical operators. Constraints also participate in overload ordering.
void Print(std::integral auto value);
void Print(std::floating_point auto value);
Constrained auto is shorthand for a function template. Separate overloads are often clearer than one large if constexpr when integral and floating-point inputs have different meanings.
Valid syntax is not a semantic guarantee
The fact that a < b compiles does not prove that it defines a consistent ordering. Likewise, syntactic copyability does not guarantee that two copies are behaviorally independent.
Standard concepts document semantic requirements as well as syntax. A custom concept should have a meaningful name, and important semantics still need tests.
Do not constrain every template more than necessary. Express the smallest real contract required by the implementation and its public API; otherwise useful types may be rejected without improving correctness.