Concepts and Constrained Templates (C++20)

Reviewed & published by Brayan K

Templates are powerful, but an unconstrained template accepts any type — and when the type is wrong, the compiler buries you in pages of errors from deep inside the instantiation. C++20 concepts fix this by letting you say, in one readable line, exactly what a type must support. By the end of this lesson you'll constrain templates with requires, use standard concepts like std::integral, write your own with requires-expressions, and enjoy error messages that finally make sense.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Think of a concept as the job requirements on a vacancy. An unconstrained template is "anyone may apply" — and you only discover a candidate can't do the job after you've hired them and everything breaks. A concept is the line "must be able to add two values" printed right on the advert: unqualified applicants are turned away at the door (compile time), with a one-line reason, instead of failing messily on the job. Standard concepts like std::integral are pre-written, commonly needed requirements you can drop straight into the advert.

1. The Problem: Unconstrained Templates

A plain template will try to compile for any type you give it. If the body does something the type doesn't support, the error appears deep inside the template, often dozens of lines long. Before C++20 the workaround was SFINAE with std::enable_if — it worked, but it was hard to write and the diagnostics were notoriously cryptic.

#include <iostream>
#include <type_traits>
using namespace std;

// The OLD way (pre-C++20): SFINAE with enable_if.
// Verbose, and the errors are brutal if you call it wrong.
template <typename T,
          typename = enable_if_t<is_integral_v<T>>>
T triple(T x) { return x * 3; }

int main() {
    cout << triple(7) << endl;      // 21  -- int is integral, OK
    // cout << triple(2.5) << endl; // ERROR: a long, cryptic SFINAE message
    return 0;
}

// ✅ Expected output:
//    21

2. Constraining with requires and Standard Concepts

A concept is a named compile-time predicate over types. The standard library ships many in the <concepts> header. You attach one to a template with a requires clause. The same code as above becomes a single readable line, and a wrong call produces a short message like "double does not satisfy integral".

#include <iostream>
#include <concepts>   // std::integral lives here
using namespace std;

// The NEW way (C++20): a requires clause with a standard concept.
// Reads like English: "this T must be integral."
template <typename T>
    requires std::integral<T>
T triple(T x) { return x * 3; }

int main() {
    cout << triple(7) << endl;       // 21  -- int satisfies integral
    // triple(2.5);  // ERROR: clear -> 'double' does not satisfy 'integral'
    return 0;
}

// ✅ Expected output:
//    21

You can make it even more concise with an abbreviated function template: write a constrained auto parameter, and the compiler creates the template parameter for you behind the scenes.

#include <iostream>
#include <concepts>
using namespace std;

// Abbreviated function template: a CONSTRAINED auto parameter.
// 'std::floating_point auto' = an implicit template param that must
// be float, double, or long double.
auto half(std::floating_point auto x) {
    return x / 2;
}

int main() {
    cout << half(3.0) << endl;   // 1.5  -- double is floating_point
    cout << half(5.0f) << endl;  // 2.5  -- float is floating_point too
    // half(10);  // ERROR: 'int' does not satisfy 'floating_point'
    return 0;
}

// ✅ Expected output:
//    1.5
//    2.5

Your turn. Add the one keyword that turns the line below into a constraint, so the function only accepts integer types:

#include <iostream>
#include <concepts>
using namespace std;

// 🎯 YOUR TURN — constrain this function so it only accepts integers.

template <typename T>
    ___ std::integral<T>     // 👉 add the keyword:  requires
T addOne(T x) { return x + 1; }

int main() {
    cout << addOne(41) << endl;   // 42
    // ✅ Expected output:  42
    // (Calling addOne(3.14) should now be a compile error.)
    return 0;
}

🧩 Reorder Challenge

These lines should define a concept and use it to constrain a function. Put them in the right order:

A. template <typename T>
B. concept Printable = requires(T a) { std::cout << a; };
C. template <Printable T>
D. void show(T a) { std::cout << a; }

Correct order: A → B → C → D

Declare the template parameter (A), define the concept with a requires-expression (B), then use the concept as the constraint (C) on the function that prints (D).

3. Writing Your Own Concept with a requires-expression

When no standard concept fits, define your own. A concept is written template<typename T> concept Name = ...;. The right-hand side is any compile-time boolean — often a requires-expression, which lists expressions that must be well-formed for the type. Note it checks validity, not values: requires(T a, T b) { a + b; } is true whenever a + b compiles for T.

#include <iostream>
#include <concepts>
#include <string>
using namespace std;

// A custom concept built from a requires-EXPRESSION.
// It is satisfied if 'a + b' is a valid expression for T.
template <typename T>
concept Addable = requires(T a, T b) {
    a + b;                 // must be well-formed
};

template <Addable T>       // use the concept as the parameter's constraint
T sum(T a, T b) { return a + b; }

int main() {
    cout << sum(2, 3) << endl;                 // 5    -- int is Addable
    cout << sum(string("ab"), string("cd"));   // abcd -- string is Addable
    cout << endl;
    return 0;
}

// ✅ Expected output:
//    5
//    abcd

4. Better Overload Resolution

Concepts also improve overloading. When several overloads match a call, the compiler prefers the more constrained one (this is called subsumption). That lets you write a generic fallback plus specialized, constrained versions, and the right one is chosen automatically — no tag dispatch required.

#include <iostream>
#include <concepts>
using namespace std;

// Two overloads. The compiler prefers the MORE constrained one.
template <typename T>
void describe(T)                         { cout << "anything" << endl; }

template <std::integral T>
void describe(T)                         { cout << "an integer" << endl; }

int main() {
    describe(42);     // an integer  -- integral overload is more constrained
    describe(3.14);   // anything    -- double is not integral, falls back
    return 0;
}

// ✅ Expected output:
//    an integer
//    anything

🔎 Deep Dive: concepts vs SFINAE

Under the hood, an unsatisfied concept simply removes a function from overload resolution — much like SFINAE did. The difference is readability and diagnostics. SFINAE hid the requirement inside a template default argument; concepts put it where you can see it and name it.

Four equivalent ways to apply a constraint, from most explicit to most concise:

template <typename T> requires std::integral<T>
void a(T x);                         // 1. trailing-ish requires clause

template <typename T>
    requires std::integral<T>
void b(T x);                         // 2. same idea, own line

template <std::integral T>
void c(T x);                         // 3. constrained template parameter

void d(std::integral auto x);        // 4. abbreviated (constrained auto)

All four mean the same thing: "T must satisfy std::integral." Pick whichever reads best.

Pro Tips

🧠 Quick Recall

1. Which standard concept matches int and long?

std::integral — from the <concepts> header.

2. Does this compile?

template <std::floating_point T>
T half(T x) { return x / 2; }
half(10);   // ?

No — int does not satisfy std::floating_point, so the call is rejected at compile time.

3. What does a requires-expression check?

That the listed expressions are well-formed (compile) for the type — not their runtime values.

Common Errors (and the fix)

📋 Quick Reference

TaskCode
Constrain a templatetemplate<typename T> requires std::integral<T>
Constrained parametertemplate<std::integral T>
Abbreviated templatevoid f(std::integral auto x);
Define a concepttemplate<typename T> concept C = ...;
requires-expressionrequires(T a) { a.size(); }
Combine conceptsstd::integral<T> || std::floating_point<T>
Header#include <concepts>

Mini-Challenge: a Numeric concept

Define a concept that accepts any number (integer or floating point), then constrain an average function with it. Combine two standard concepts with ||.

#include <iostream>
#include <concepts>
using namespace std;

// 🎯 MINI-CHALLENGE: a constrained "average" function
// 1. Define a concept  Numeric  that is satisfied when T is
//    std::integral OR std::floating_point.
//    Hint:  concept Numeric = std::integral<T> || std::floating_point<T>;
// 2. Write  average(T a, T b)  constrained by Numeric that returns (a + b) / 2.
// 3. In main(): call it with two ints and with two doubles.
//
// ✅ Example output:
//    5
//    3.5

// your code here

int main() {
    // your code here
    return 0;
}

🎉 Lesson Complete

Practice quiz

What C++ standard introduced concepts as a core language feature?

  • C++20
  • C++23
  • C++14
  • C++17

Answer: C++20. Concepts became part of the language in C++20, after years as a Technical Specification.

What is a concept in C++20?

  • A replacement for virtual functions
  • A macro that expands to a type
  • A named, compile-time predicate that constrains template parameters
  • A new kind of runtime polymorphism

Answer: A named, compile-time predicate that constrains template parameters. A concept is a named boolean predicate evaluated at compile time that says what a type must support.

Which standard concept matches any integer type such as int or long?

  • std::whole
  • std::integral
  • std::numeric
  • std::is_int

Answer: std::integral. std::integral, from the <concepts> header, is satisfied by the built-in integer types.

What is the main advantage of concepts over SFINAE and enable_if?

  • They remove the need for templates entirely
  • They allow runtime type checking
  • They run faster at runtime
  • They produce far clearer compiler error messages and read declaratively

Answer: They produce far clearer compiler error messages and read declaratively. Concepts state intent directly, so a violation gives a short readable error instead of a wall of SFINAE noise.

What does a requires-expression like requires(T a, T b) { a + b; } check?

  • That the expression a + b is valid for type T (it is well-formed)
  • That a and b are equal
  • That T has a constructor
  • That a + b returns void

Answer: That the expression a + b is valid for type T (it is well-formed). A requires-expression checks only that the listed expressions are well-formed for the type, not their values.

Which keyword introduces a constraint clause on a template?

  • where
  • requires
  • assert
  • constrain

Answer: requires. The requires clause attaches a constraint, e.g. template<typename T> requires std::integral<T>.

What does an abbreviated function template like void f(std::integral auto x) mean?

  • x must be a concrete int
  • x is captured by reference
  • f is a runtime virtual function
  • x is a constrained template parameter satisfying std::integral

Answer: x is a constrained template parameter satisfying std::integral. Writing a constrained 'auto' parameter creates an implicit template parameter restricted by that concept.

If you call a function constrained by std::floating_point with an int argument, what happens?

  • It compiles but produces undefined behavior
  • It silently converts the int to double
  • The call is rejected at compile time because int does not satisfy the constraint
  • It throws a runtime exception

Answer: The call is rejected at compile time because int does not satisfy the constraint. Constraints are checked at compile time; an unsatisfied concept removes the function from overload resolution.

How do you define a concept named Addable for a type T?

  • using Addable = T + T;
  • template<typename T> concept Addable = requires(T a, T b) { a + b; };
  • concept Addable<T> { return T + T; }
  • define Addable(T) { a + b; }

Answer: template<typename T> concept Addable = requires(T a, T b) { a + b; };. A concept is written template<typename T> concept Name = constraint-expression; here a requires-expression.

When two overloads both match but one has a stricter (more constrained) concept, which is chosen?

  • The more constrained overload is preferred
  • The first one declared
  • The less constrained one
  • It is always ambiguous

Answer: The more constrained overload is preferred. Constraint subsumption makes the compiler prefer the more constrained overload when both are viable.

Continue this course

Frequently asked questions

What problem do C++20 concepts solve?

Before concepts, constraining a template meant SFINAE tricks with std::enable_if, which were verbose and produced enormous, hard-to-read error messages. Concepts let you state directly what a type must support, so the compiler can reject bad calls with a short, clear message at the point of use.

What is the difference between a requires clause and a requires-expression?

A requires clause attaches a constraint to a template, for example template<typename T> requires std::integral<T>. A requires-expression is the thing you can write after that keyword, like requires(T a) { a.size(); }, which yields true only if every listed expression is well-formed for the type.

Do I have to write my own concepts?

Often not. The standard library ships many ready-made concepts in the <concepts> header, such as std::integral, std::floating_point, std::same_as, std::convertible_to, and std::equality_comparable. You write your own concept only when you need to express a requirement the standard ones do not cover.

What is an abbreviated function template?

When you write a constrained auto parameter, like void print(std::integral auto x), the compiler turns it into a template with an implicit, constrained type parameter. It is the most concise way to write a constrained generic function.

Are concepts checked at compile time or run time?

Entirely at compile time. A concept is a compile-time predicate over types. If a type fails a constraint, the function is simply removed from overload resolution, and you get a diagnostic before the program ever runs. There is zero runtime cost.

Related lessons