C++20 Concepts: Constrain Templates for More Useful Errors
Learn how named concepts and requires expressions reject unsuitable template arguments at the interface, with exact examples of satisfaction, failure, and overload selection.
KnowledgeGate Team
Exam prep & CS education

An unconstrained function template can accept a bad candidate and fail several instantiation layers away from the call. You then have to decode library and operator errors instead of seeing the missing requirement. C++20 concepts offer a concrete route from named predicates and requires expressions to failures that identify the rejected operation. Compiler wording still varies, and concepts make interfaces explicit rather than guaranteeing one perfect message. If templates, types, or basic syntax are unfamiliar, begin with the C++ Tutorial: Complete Learning Path.
1. C++20 Concepts: what changes before template instantiation
A concept is a named compile-time predicate over template arguments. For a constrained call, the compiler deduces T, forms and checks the associated constraints, keeps the candidate only if they are satisfied, and then instantiates the selected function body. A rejected candidate is not automatically a program error because another viable overload may win.
Compare template<class T> T old_average(T a, T b) with template<Averagable T> T average_pair(T a, T b). The first declaration says nothing about required operators. The second puts a named boundary at the interface, before the body is entered.
Concepts test compile-time expressions and type relationships. They cannot prove that a divisor is non-zero, a container has three elements, arithmetic will not overflow, or a custom operator follows the mathematical law suggested by its name.
2. C++20 constraint syntax: four equivalent places to state intent
The same logical requirement can appear in four forms. These functions coexist because their names differ:
#include <concepts>
template<std::integral T>
T twice_a(T x) { return x + x; }
auto twice_b(std::integral auto x) { return x + x; }
template<class T>
requires std::integral<T>
T twice_c(T x) { return x + x; }
template<class T>
T twice_d(T x) requires std::integral<T> { return x + x; }For input 7, each returns 7 + 7 = 14. For 2.5, each rejects the call because double does not satisfy std::integral.
Form | Best reading use | Constraint location |
|---|---|---|
Constrained type parameter | Requirement belongs directly to | Template parameter list |
Abbreviated function template | A short, local generic interface | Function parameter list |
Leading requires-clause | Constraint should be visible before the declaration | After template parameter list |
Trailing requires-clause | Constraint reads naturally after the signature | After function declarator |
When the logic is identical, the choice is mainly about interface clarity. The <concepts> header also provides std::signed_integral, std::floating_point, std::same_as, and std::convertible_to. The Coding & DSA Courses for Placements route can support wider, systematic language practice.
3. C++20 Concepts worked example: Averagable succeeds at 21 and fails at x / 2
Averagable requires ordering, x + x, and x / 2 before accepting T:
#include <concepts>
#include <iostream>
#include <string>
template<class T>
concept Averagable =
std::totally_ordered<T> &&
requires(T x) {
{ x + x } -> std::same_as<T>;
{ x / 2 } -> std::same_as<T>;
};
template<Averagable T>
T average_pair(T a, T b) {
return (a + b) / 2;
}
int main() {
std::cout << average_pair(18, 24) << '\n';
std::cout << average_pair(18.0, 25.0) << '\n';
}For average_pair(18, 24), deduction gives T=int. Ordering, x+x, and x/2 satisfy the concept. The body computes (18+24)/2 = 42/2 = 21. For average_pair(18.0, 25.0), T=double, and the result is (18.0+25.0)/2 = 43.0/2 = 21.5.
Now try average_pair(std::string{"18"}, std::string{"24"}). A std::string is ordered, and x+x produces another string, but x/2 is invalid. Therefore Averagable<std::string> is false and the candidate is not viable. A likely diagnostic has a “constraints not satisfied” note followed by the invalid x / 2 requirement, though exact wording varies by compiler.

4. requires expressions: simple, type, compound, and nested requirements
One concept can combine simple, type, compound, and nested requirements:
template<class T>
concept ScoreBuffer = requires(T& xs, std::size_t i) {
typename T::value_type;
xs.clear();
{ xs.size() } -> std::convertible_to<std::size_t>;
{ xs[i] } -> std::convertible_to<int>;
requires std::same_as<typename T::value_type, int>;
};xs.clear() is a simple requirement. typename T::value_type is a type requirement. The two brace-and-arrow lines are compound requirements, while the final requires line is nested. These expressions are checked for validity and declared result constraints. They are not executed during concept checking.
std::vector<int>{4,7,9} satisfies this structure. std::list<int>{4,7,9} fails at xs[i], and std::vector<double>{4.0,7.0,9.0} fails the nested value_type check. Even for the passing vector, ScoreBuffer cannot prove that a chosen runtime index is within the current size.
5. C++20 concept subsumption: choose the more constrained overload
Standard concept refinement lets partial ordering recognise the stronger requirement:
template<std::integral T>
const char* classify(T) { return "integral"; }
template<std::signed_integral T>
const char* classify(T) { return "signed integral"; }For classify(-5), both constraints hold. Because std::signed_integral refines std::integral, the more constrained overload wins and returns "signed integral". For classify(5u), std::integral<unsigned int> is true but std::signed_integral<unsigned int> is false, so the result is "integral".
Do not spell supposedly equivalent constraints as unrelated Boolean expressions and expect the compiler to infer human intent. Named concepts and direct refinement give constraint normalization and subsumption a clearer relationship.

6. C++20 constraint errors: read the failed requirement from the call site inward
Use three steps when a constrained call fails:
Start at the rejected call and identify the inferred
T.Find the candidate note that names the failed concept.
Inspect the first unsatisfied atomic or expression requirement.
For the string call, the decisive fact is that Averagable<std::string> reaches an invalid x / 2, not the surrounding overload noise. With old_average(std::string{"18"}, std::string{"24"}), the unconstrained template is selected and compilation instead fails on (a+b)/2 inside its body. The constrained version moves the missing operation into the public interface.
A representative Clang diagnostic, with source locations removed, makes that requirement chain explicit:
error: no matching function for call to 'average_pair'
note: candidate template ignored: constraints not satisfied [with T = std::string]
note: because 'std::string' does not satisfy 'Averagable'
note: because 'x / 2' would be invalid: invalid operands to binary expression ('std::string' and 'int')The rules for associated constraints, satisfaction, and invalid requirements inside templated requires expressions are defined in WG21's 2020 final C++20 working draft N4860, especially [temp.constr] and [expr.prim.req]. The compiler reports the failed requirement, but GCC, Clang, and MSVC need not use identical wording or line order.
7. C++20 Concepts traps that exams and interviews exploit
Consider short a=6, b=10. In Averagable<short>, both a+b and a/2 have type int after integral promotion. They fail std::same_as<short> even though the arithmetic looks reasonable. Replacing same_as with convertible_to accepts the expressions, but the looser rule may permit narrowing. The right predicate depends on the interface contract.
Structural validity is not numerical meaning. average_pair(18,25) passes for int, but computes (18+25)/2 = 43/2 = 21, not 21.5. The concept also cannot mechanically prevent overflow in a+b or misleading user-defined operator behaviour.
Assessment tasks can ask you to identify a viable call, locate a failing requirement, choose the selected constrained overload, rewrite an enable-if style interface with a named concept, or distinguish a requires-clause from a requires-expression. Use the Programming Languages Survey as a broader revision hub.
8. C++20 Concepts: the short version and next step
Name the smallest honest requirement.
Prefer a reusable standard or custom concept.
Put it at the template boundary.
Test one passing type and one failing type.
Read failures from the concept to the first unmet expression.
The key results are average_pair(18,24)=21, the string call fails at x/2, and classify(-5) chooses the signed overload. Continue with the C++ Programming Course for structured practice in concepts, MCQs, and coding.
Keep learning
Templates in C++: Reusable Parameterized Code with Worked Examples
Learn how one C++ template produces type-specific functions and classes. Follow exact calls, calculations, deduction failures, and practical corrections.
Friend Functions in C++: Controlled Class Access with a Worked Example
See how a non-member friend function can validate and update two Wallet objects while private balances stay hidden from ordinary callers. Includes runnable C++ and an exact trace.
Data Types in C++: Fundamental and Derived Types with Worked Examples
Learn how C++ fundamental types differ from arrays, pointers, references, and function types. Trace one complete program, then test the conversion traps.
C++ Constructors and Object Initialization: Techniques, Order and a Worked Trace
Learn how C++ chooses default, parameterised, delegating and copy constructors, then trace member states and initialization order through complete code.





