<< Home

Chapter 14: Concepts and Constraints

Concepts let you describe the expectations that a template places on its arguments. Instead of waiting for a deep instantiation error, you give the compiler a clear statement of intent, and it checks constraints before instantiating. This leads to better diagnostics, clearer interfaces, and more robust generic code. This chapter explores the purpose of constraints, the forms of requires, how to craft custom concepts, how to use constrained auto, and how to move from older SFINAE techniques to modern expressions.

Why constraints improve diagnostics

Without constraints, templates accept almost anything until substitution reaches a point where something fails to compile. The resulting error may refer to a detail buried far inside another template. A constraint moves that check to the boundary of the template so that failure happens early and the message points directly to the violated assumption.

Failing early at the call site

When a constraint is not satisfied, the compiler does not instantiate the template at all. This puts responsibility on the caller rather than the internals, which helps pinpoint the exact mismatch. Constraints therefore serve both as documentation and enforcement.

template <class T>
requires std::integral<T>
T half(T x) { return x / 2; }

half(3.0);  // error at the call site; double is not integral
πŸ’‘ Constrain templates at their boundary rather than deep inside. This keeps errors clear and directs readers toward the intended usage.

Using requires

A requires clause attaches a constraint to a declaration. A requires expression checks whether a set of operations or properties is valid for a given set of types or values. Both are central to expressing intent about template parameters.

Requires clausess

Use a requires clause after the template parameter list or after the function signature to state requirements on arguments. Clauses can combine concepts with logical operators such as && and || for more expressive contracts.

template <class T>
requires std::copy_constructible<T> && std::movable<T>
void swap_em(T& a, T& b) {
  T tmp = a;
  a = b;
  b = tmp;
}

Requires expressions

A requires expression describes exact operations that must be valid in a context. It can check expression validity, return types, and nested constraints while remaining purely compile time logic.

template <class T>
concept Addable = requires(T a, T b) {
  { a + b } -> std::same_as<T>;
  { a += b };
};
⚠️ Avoid overly strict requires expressions; check only what your algorithm truly needs rather than everything the type might support.

Defining custom concepts

Concepts let you name recurring patterns and use them in many templates. Custom concepts turn informal requirements into reusable constraints that improve readability and maintainability.

Concept definitions

A concept is a compile time predicate that returns true or false. You can compose concepts with logical operators, and you can combine broad concepts with narrow ones to refine meaning.

template <class T>
concept NumberLike =
  std::integral<T> ||
  std::floating_point<T>;

template <NumberLike T>
T scale(T x, T factor) { return x * factor; }

Traits and concepts working together

Use type traits to query properties such as nested typedefs, constant expressions, or member presence, then wrap those checks in a concept. This preserves the benefits of existing trait infrastructure while giving you clear constraint boundaries.

template <class T>
concept HasValueType = requires { typename T::value_type; };
πŸ’‘ Keep concepts simple and focused. A small well named concept clarifies code far more than a large compound expression scattered across many templates.

Constrained auto and abbreviated templates

C++20 introduced constrained auto which allows a concept to appear directly in parameter lists. This is known as an abbreviated template. It reduces verbosity by placing constraints exactly where parameters appear rather than at the top of a declaration.

Using concepts with auto

You can place a concept before auto to constrain a parameter inline. The compiler treats this as shorthand for a template declaration with the concept as a constraint.

void print_twice(std::integral auto x) {
  std::cout << x << " " << x;
}

Abbreviated templates

Abbreviated templates reduce noise in simple function templates. They also help reveal intent at the point of use. Keep this style for shorter functions and reserve fuller template forms for complex contracts.

std::totally_ordered auto max3(std::totally_ordered auto a,
                               std::totally_ordered auto b,
                               std::totally_ordered auto c) {
  return std::max(std::max(a, b), c);
}
⚠️ Abbreviated templates cannot express every constraint relationship. Use a full template header when parameters depend on each other.

Migrating SFINAE to concepts

Older template code used SFINAE to remove overloads from consideration when substitution failed. Concepts make this intent explicit and readable. Migrating from SFINAE to concepts usually simplifies code, reveals intent, and trims dense helper machinery.

Replacing enable_if patterns with constraints

Patterns such as std::enable_if_t<cond, T> are replaced with straightforward requires clauses or concept constrained parameters. This removes boilerplate and puts the condition where people expect to find it.

// old style
template <class T,
          class = std::enable_if_t<std::integral<T>::value>>
T f(T x);

// modern style
template <std::integral T>
T f(T x);

Trait based detection

Type trait detection used to chain specializations, void_t patterns, and fallback overloads. Concepts now express the desired property directly using requires expressions and combine naturally with the rest of the type system.

// old detection idiom
template <class, class = void>
struct HasSize : std::false_type {};

template <class T>
struct HasSize<T, std::void_t<decltype(std::declval<T>().size())>>
  : std::true_type {};

// modern concept
template <class T>
concept HasSizeC = requires(T t) { t.size(); };
πŸ’‘ When updating code, migrate traits or SFINAE expressions first, then apply concepts at call boundaries. This produces the clearest improvement in diagnostics and structure.