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.
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 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:
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) {' '} 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.
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.
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.
Define a concept that accepts any number (integer or floating point), then constrain an average function with it. Combine two standard concepts with || .
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.