Concepts and Constrained Templates (C++20)
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.
Learn Concepts and Constrained Templates (C++20) in our free C++ course — an interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, 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.
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 ".
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.
Your turn. Add the one keyword that turns the line below into a constraint, so the function only accepts integer types:
🧩 Reorder Challenge
These lines should define a concept and use it to constrain a function. Put them in the right order:
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 .
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.
🔎 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:
All four mean the same thing: " T must satisfy std::integral ." Pick whichever reads best.
Pro Tips
- 💡 Reach for standard concepts first: std::integral , std::floating_point , std::same_as , std::convertible_to , and std::equality_comparable cover most needs.
- 💡 Prefer constrained auto for short functions — void f(std::integral auto x) is the most concise form.
- 💡 Name concepts for what they require , like Hashable or Drawable , so call sites read clearly.
- 💡 A requires-expression checks validity, not truth: it only asks "does this expression compile?", never "is it true?".
🧠 Quick Recall
1. Which standard concept matches int and long?
No — int does not satisfy std::floating_point , so the call is rejected at compile time.
That the listed expressions are well-formed (compile) for the type — not their runtime values.
Common Errors (and the fix)
- Forgetting #include <concepts> : the standard concepts like std::integral live there. Include it before using them.
- Confusing a concept with its constraint: a concept is defined with concept Name = ...; and used with a requires clause or as template<Name T> .
- Expecting a requires-expression to test values: requires(T a) { a > 0; } only checks that a > 0 compiles — it never checks that a is positive.
- Compiling without C++20: concepts need -std=c++20 (or later). Older standards reject the syntax entirely.
- Over-constraining: demanding more than the body actually uses needlessly rejects valid types. Constrain to exactly what you use.
📋 Quick Reference
Task
Code
Constrain a template
template<typename T> requires std::integral<T>
Constrained parameter
template<std::integral T>
Abbreviated template
void f(std::integral auto x);
Define a concept
template<typename T> concept C = ...;
requires-expression
requires(T a) { a.size(); }
Combine concepts
std::integral<T> || std::floating_point<T>
Header
#include <concepts>
Frequently Asked Questions
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 || .
🎉 Lesson Complete
- ✅ A concept is a named compile-time predicate that constrains template parameters
- ✅ Attach one with a requires clause or template<Concept T>
- ✅ Standard concepts like std::integral and std::floating_point cover common cases
- ✅ A requires-expression checks that expressions are well-formed for a type
- ✅ Constrained auto gives the most concise abbreviated function templates
- ✅ Concepts give clear errors and prefer the more constrained overload
- ✅ Next lesson: the Ranges library — compose lazy, readable algorithms
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
- Previous: Final Project
- Next: The Ranges Library (C++20)