C++20 Concepts | Making Template Error Messages Readable
이 글의 핵심
When you pass the wrong type to a template function, compilers used to print a long instantiation stack that was hard to read. Concepts—a C++20 feature that names constraints template arguments must satisfy—declare what a template expects so the compiler reports which constraint failed first. This post walks through the idea and examples step by step.
Introduction: template errors were too cryptic
“The compiler doesn’t know the type, so I get 100 lines of errors”
When you pass a wrong type to a template function, the compiler used to print a long instantiation stack, which made errors hard to read. Concepts (a C++20 feature that names the conditions template arguments must satisfy) let you declare what types a template expects, so if a constraint fails, the compiler reports which constraint failed first. Roughly, it is like writing “only integers go here”; if you pass the wrong kind of type, you get “not an integer” immediately. Using standard concepts such as std::integral and std::ranges::range states intent, improves error messages, and makes overload resolution and specialization easier.
This article covers:
- What a Concept is, and how it differs from legacy SFINAE (Substitution Failure Is Not An Error—when substitution fails for a template argument, that overload is discarded without a hard error) /
enable_if - How to pick standard concepts and apply them in real code
- Answers to common questions such as “integers only” or “containers only”
In the problem code, add accepts any type T, so passing a vector makes the compiler instantiate T = std::vector<int> and then fail while looking for a + b. The error is not simply “no operator+”; the template instantiation stack is long, so it is hard to see why this type is invalid. With Concepts you constrain T (e.g. “integers only”) so passing a vector fails early with something like “does not satisfy std::integral”, and you see which constraint broke first. In production, adopting Concepts when template errors balloon speeds up diagnosis.
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
add(std::vector<int>{}, std::vector<int>{}); // no + for vector
// error: 50+ lines of instantiation noise...
}
Fix with Concepts:
// After pasting: g++ -std=c++20 -o concepts_add concepts_add.cpp && ./concepts_add
#include <concepts>
#include <iostream>
template <std::integral T> // integers only
T add(T a, T b) {
return a + b;
}
int main() {
std::cout << add(3, 5) << "\n"; // OK → 8
// add(3.14, 2.0); // uncomment → compile error: std::integral not satisfied
return 0;
}
Output: a single line printing 8.
If the Concept is not satisfied, the compiler reports the constraint first. Example messages (vary by toolchain):
error: no matching function for call to 'add(double, double)'
note: template constraint not satisfied
note: 'double' does not satisfy 'std::integral'
Build: examples assume C++20. Use g++ -std=c++20 or Clang -std=c++20.
Error messages: before / after
Before (no Concept): calling add(std::vector<int>{}, std::vector<int>{})
// example output
error: no match for 'operator+' (operand types are 'std::vector<int>' and 'std::vector<int>')
34 | return a + b;
| ~~^~~
note: candidate: 'operator+(int, int)' (built-in)
note: candidate: 'operator+(double, double)' (built-in)
....(dozens of lines of template instantiation stack)
After (with Concept): same call
error: no matching function for call to 'add(std::vector<int>, std::vector<int>)'
note: template constraint not satisfied
note: 'std::vector<int>' does not satisfy 'std::integral'
Difference: with Concepts you immediately see “does not satisfy std::integral”, so you know this function takes integers only. Before, you might see “no operator+”, but understanding why a vector is wrong required walking the whole stack.
Concepts vs SFINAE and enable_if
Before C++20, SFINAE or std::enable_if expressed “use this overload only for these types.” Behavior is similar, but readability and diagnostics differ a lot.
flowchart LR
subgraph old[SFINAE / enable_if]
O1[verbose syntax] --> O2[long instantiation stack]
O2 --> O3[harder root cause]
end
subgraph new["Concepts (C++20)"]
N1[clear intent in one line] --> N2[constraint failure surfaced early]
N2 --> N3[faster diagnosis]
end
old -.->|improvement| new
| Aspect | SFINAE / enable_if | Concepts (C++20) |
|---|---|---|
| Readability | Verbose, e.g. typename std::enable_if<std::is_integral_v<T>>::type* = nullptr | template <std::integral T> states intent in one line |
| Errors | “Substitution failed” or long stacks | Messages like “T does not satisfy std::integral” |
| Composition | && / || combinations get very heavy | requires A<T> && B<T> stays readable |
| Overloads | Possible but verbose | Clean splits for integral vs floating-point, etc. |
Summary: prefer Concepts for new C++20+ code. When you see enable_if in legacy code, read it as “how we used to express constraints.”
enable_if vs Concepts — side-by-side
With enable_if (pre–C++17 style):
#include <type_traits>
// add for integers only — enable_if on return type
template <typename T>
typename std::enable_if<std::is_integral_v<T>, T>::type
add(T a, T b) {
return a + b;
}
// add for floating-point — separate overload
template <typename T>
typename std::enable_if<std::is_floating_point_v<T>, T>::type
add(T a, T b) {
return a + b;
}
Issues:
typename std::enable_if<...>::typeon the return type is long; the intent (“only for these types”) is buried.- Calling
add(std::vector<int>{}, std::vector<int>{})can yield no matching function after both overloads fail substitution, without clearly saying which condition was missing.
Same logic with Concepts:
#include <concepts>
template <std::integral T>
T add(T a, T b) { return a + b; }
template <std::floating_point T>
T add(T a, T b) { return a + b; }
Benefits:
- One line per intent: “integers only,” “floating-point only.”
- Bad calls report which Concept failed (
std::integral,std::floating_point, etc.).
Three real-world pain scenarios
Scenario 1: under-documented library API
A teammate’s process_data(T& data) was called with std::unique_ptr<int> and produced an 80-line error. Thirty minutes later you discover deep in the code that T must be copyable. With template <std::copyable T>, you might have gotten a single line: does not satisfy std::copyable.
Scenario 2: wrong iterators to a generic algorithm
You pass iterators from std::list into sort_range(first, last), but std::sort needs random_access_iterator. The error starts with “no match for operator-…” and it is unclear why list fails. With template <std::random_access_iterator It>, you get “does not satisfy random_access_iterator” up front.
Scenario 3: overlapping enable_if overloads
serialize(T) had three enable_if branches for int, float, and string, and tracing which version applied was painful. Splitting with std::integral, std::floating_point, and std::convertible_to<T, std::string> makes the selection rules visible in code.
After reading this post you will:
- Understand basic Concept and
requiressyntax. - Know when to use standard concepts (
std::integral,std::copyable, …). - Use template constraints to improve errors and clarify overloads/specializations.
Practical note: this article draws on real issues and fixes from large C++ codebases, including pitfalls and debugging tips not always found in books.
Table of contents
- Real-world problem scenarios
- What is a Concept?
- The
requiresclause - Standard concepts — full example
- Applying to function templates
- Practical use
- Common issues and fixes
- Best practices
- Production patterns
- Performance and compile time
- Summary and FAQ
- Implementation checklist
1. Real-world problem scenarios
Typical situations without Concepts:
| Situation | Symptom | After Concepts |
|---|---|---|
| Wrong type passed | 50–100 line instantiation stack | One line: “does not satisfy std::integral” |
| Unclear API intent | Hard to see allowed types from code | template <std::copyable T> makes it obvious |
| Complex overload rules | Many enable_ifs — hard to trace | Clear separation per Concept |
| Iterator vs range confusion | Cryptic errors applying sort to list | Immediate “needs random_access_iterator” |
2. What is a Concept?
Conditions on types
A Concept expresses conditions a type must satisfy: “this function takes only integer types,” “this class accepts only copyable types.” It documents requirements and lets the compiler check them at compile time. Failure is reported at the call site, not deep inside unrelated code, so you catch misuse while writing code, not at runtime.
flowchart TD
A[Template call] --> B{Concept constraint check}
B -->|satisfied| C[Instantiation]
B -->|failed| D[Immediate error: which constraint failed]
C --> E[Normal execution]
When this helps:
- Clarify APIs: “only
int,long, …” visible from the signature. - Better errors: passing a
vectoryields “does not satisfy std::integral” instead of a deep stack. - Overload design: split integer vs floating-point implementations under one name.
Below, square uses std::integral (“integers only”) so it works for int, unsigned, long, char, etc.; half uses std::floating_point for float, double, long double. The same names square / half select different templates; add(3.14, 2.0) fails the integral constraint at compile time. What used to need a long enable_if becomes one line.
#include <concepts>
// integers only (int, unsigned, long, char, ...)
template <std::integral T>
T square(T x) {
return x * x;
}
// floating-point only (float, double, long double)
template <std::floating_point T>
T half(T x) {
return x / 2;
}
Why split?
squarecould take integers and floats, but constraining to integers makes “integer-only” explicit and simplifies a later floating-point overload if you add one.halftakes floats because for integers,half(3)is integer division (3/2→1), whilehalf(3.0)is1.5. Separation matches intent.
Two common syntactic forms
You can apply Concepts in two main ways. The abbreviated form is easiest to read; for a single concept, prefer template <Concept T>.
template <typename T>
requires std::integral<T> // requires clause (good for combining conditions)
T add(T a, T b) { return a + b; }
// Equivalent (abbreviated) — prefer when there is only one concept
template <std::integral T>
T add(T a, T b) { return a + b; }
Caveats:
- The abbreviated form works when you have one concept. For
requires A<T> && B<T>, use arequiresclause. - A
requiresclause followstemplate <typename T>on the next line:requires condition.
3. The requires clause
requires is used in two ways:
requiresclause: combine existing Concepts or predicates — “this template is only valid when…”requiresexpression: define new Concepts by requiring certain operations or members onT.
Below we separate these.
Compound requirements (combining conditions)
You can combine standard concepts with sizeof, std::is_*, etc., using && and ||. That fits needs like “integer and at least four bytes.”
#include <concepts>
#include <type_traits>
template <typename T>
requires std::integral<T> && (sizeof(T) >= 4)
T add(T a, T b) {
return a + b;
}
// int, long, long long OK; char, short may be excluded if sizeof < 4
When to use:
- One Concept is not enough; you need size or traits too.
- You need
Concept1<T> && Concept2<T>at once.
Caution: sizeof(T) >= 4 is platform-dependent. int is often 4 bytes, but some embedded targets use 2. To spell “at least 4 bytes” portably, list types explicitly or document platform assumptions.
requires expression (behavioral requirements)
To require that certain operations exist on T, define a Concept with a requires expression: requires (params) { expressions; } checks that the expressions are well-formed (they are not actually run).
// runnable example
template <typename T>
concept Addable = requires(T a, T b) {
a + b; // a + b must be a valid expression
};
template <Addable T>
T add(T a, T b) {
return a + b;
}
Meaning: requires(T a, T b) { a + b; } means “for values a, b of type T, a + b must be valid.”
int,double→ Addable satisfied.std::vector<int>→ fails →add(vector{}, vector{})errors with a short “Addable not satisfied” instead of a huge stack.
When there is no standard concept: define reusable Concepts for domain rules like “has +” or “has size().” (Custom Concepts are covered in part #22-2.)
Type requirements (nested types)
To require that T has a nested type, use typename T::name inside requires. Useful for STL-like containers (value_type, iterator, …).
#include <iostream>
#include <typeinfo>
template <typename T>
concept HasValueType = requires {
typename T::value_type; // T::value_type must exist
};
// e.g. vector, list, set all have value_type
template <HasValueType C>
void print_element_type() {
std::cout << typeid(typename C::value_type).name() << '\n';
}
Why typename? T::value_type is a dependent name; the compiler needs typename to know it names a type.
4. Standard concepts — full example
The <concepts> header defines concepts for type categories and capabilities. Prefer standard concepts over ad-hoc predicates when they fit.
<concepts> — concepts you will use often
| Concept | Meaning | When to use |
|---|---|---|
std::integral<T> | Integer types | Numeric APIs that must be integers |
std::floating_point<T> | float / double / long double | Floating-point-only APIs |
std::copyable<T> | Copy construct/assign | Types stored in vector<T>, etc. |
std::movable<T> | Move construct/assign | Move-only or move-preferred APIs |
std::default_constructible<T> | Default construction | Need T t; |
std::same_as<T, U> | T and U are the same type | Two template parameters must match |
std::convertible_to<T, U> | T converts to U | Before implicit conversions |
std::arithmetic<T> | Integer or floating-point | Generic arithmetic |
Note: std::copyable<T> implies std::movable<T>. For “move but not copy,” combine e.g. std::movable<T> && !std::copyable<T>.
Runnable example: standard concepts + requires
// concepts_demo.cpp — standard concepts + requires
#include <concepts>
#include <ranges>
#include <vector>
#include <list>
#include <iostream>
// 1) std::integral — integers only
template <std::integral T>
T mod(T a, T b) {
return a % b;
}
// 2) std::floating_point — floats only
template <std::floating_point T>
T reciprocal(T x) {
return T{1} / x;
}
// 3) requires clause — compound condition
template <typename T>
requires std::integral<T> && (sizeof(T) >= 4)
T safe_multiply(T a, T b) {
return a * b;
}
// 4) std::copyable — copyable types only
template <std::copyable T>
void push_twice(std::vector<T>& v, const T& x) {
v.push_back(x);
v.push_back(x);
}
// 5) std::ranges::range — iterable container
template <std::ranges::range R>
void print_range(const R& r) {
for (const auto& x : r)
std::cout << x << " ";
std::cout << "\n";
}
// 6) requires — range whose elements are arithmetic
template <typename R>
requires std::ranges::range<R> && std::arithmetic<std::ranges::range_value_t<R>>
double sum(const R& r) {
double acc = 0;
for (const auto& x : r) acc += static_cast<double>(x);
return acc;
}
int main() {
std::cout << mod(7, 3) << "\n"; // 1 (int)
std::cout << reciprocal(2.0) << "\n"; // 0.5
std::cout << safe_multiply(5, 6) << "\n"; // 30
std::vector<int> v;
push_twice(v, 42);
print_range(v); // 42 42
std::cout << sum(std::list<int>{1,2,3}) << "\n"; // 6
}
Output:
1
0.5
30
42 42
6
Takeaways:
mod:%only on integers viastd::integral.reciprocal: division rules viastd::floating_point.safe_multiply:requiresfor “integral and ≥4 bytes.”push_twice:std::copyableforpush_back.print_range:std::ranges::rangeforvector,list, C arrays, …sum: range + arithmetic element type.
Hierarchy of standard concepts
Many standard concepts form a hierarchy: satisfying a broader concept often implies narrower ones.
flowchart TD
subgraph type[Type classification]
A["std::integral"] --> C["std::arithmetic"]
B["std::floating_point"] --> C
C --> D["std::totally_ordered"]
end
subgraph object[Object properties]
E["std::movable"] --> F["std::copyable"]
G["std::default_constructible"]
end
std::arithmetic:std::integralorstd::floating_point.std::copyable: includesstd::movable.std::totally_ordered:<,<=,>,>=,==,!=available.
Ranges concept hierarchy
Concepts in <ranges> follow iterator capabilities:
flowchart TD R["std::ranges::range"] --> IR["std::ranges::input_range"] IR --> FR["std::ranges::forward_range"] FR --> BR["std::ranges::bidirectional_range"] BR --> RAR["std::ranges::random_access_range"] RAR --> CR["std::ranges::contiguous_range"]
| Concept | Meaning | Example |
|---|---|---|
input_range | Forward read once | std::istream_view |
forward_range | Multi-pass | std::forward_list |
bidirectional_range | Prev/next | std::list |
random_access_range | Indexing | std::vector, std::deque |
contiguous_range | Contiguous storage | std::vector, C arrays |
Choosing:
- “Just iterate” →
std::ranges::rangeorinput_range. - “Need indexing” →
random_access_range. - “Need contiguous bytes” (e.g. binary serialization) →
contiguous_range.
Example: copyable storage, convertible parameters
#include <concepts>
#include <vector>
template <std::copyable T>
void store(T value) {
std::vector<T> v;
v.push_back(value); // copyable types only
}
template <typename T, typename U>
requires std::convertible_to<T, U>
U convert(T t) {
return static_cast<U>(t);
}
// Illustrates “only when T converts to U” — real APIs often take explicit template args or a single-parameter design.
Note: the sample convert omits how U is deduced; it only shows the convertible_to constraint pattern.
5. Applying to function templates
Overloads — splitting “integer” vs “floating-point”
Use Concepts to split overloads by type family. The compiler picks the overload whose constraints fit best; add(1, 2) uses the integral overload, add(1.0, 2.0) the floating-point one.
template <std::integral T>
T add(T a, T b) {
return a + b;
}
template <std::floating_point T>
T add(T a, T b) {
return a + b;
}
// add(3, 5) → integral; add(3.0, 5.0) → floating_point
Note: int satisfies std::integral, double satisfies std::floating_point; the sets are disjoint, so no ambiguity for homogeneous calls. For mixed calls like add(3, 5.0), define conversion rules, a common-type overload, or require explicit casts.
With auto — return type deduction
Constrained templates can still use auto returns when the result type follows from T.
template <std::integral T>
auto square(T x) {
return x * x; // return type follows T
}
6. Practical use
Example 1: numeric library
Use std::arithmetic when you want both integers and floats in one function.
#include <concepts>
#include <cmath>
// absolute value for any arithmetic type
template <std::arithmetic T>
T safe_abs(T x) {
if constexpr (std::integral<T>) {
return x < 0 ? -x : x;
} else {
return std::abs(x);
}
}
// safe_abs(-3) → 3
// safe_abs(-3.14) → 3.14
Why std::arithmetic? std::integral excludes double; std::floating_point excludes int. std::arithmetic covers both.
Example 2: containers and ranges — std::ranges::range
Restrict to “anything iterable” with std::ranges::range: vector, list, C arrays, custom types with begin/end.
#include <ranges>
#include <iostream>
#include <vector>
#include <list>
// runnable example
template <typename R>
requires std::ranges::range<R>
void print(const R& r) {
for (const auto& x : r) {
std::cout << x << " ";
}
std::cout << "\n";
}
int main() {
print(std::vector<int>{1, 2, 3});
print(std::list<double>{1.0, 2.0});
int arr[] = {10, 20, 30};
print(arr);
}
Summary:
std::ranges::range<R>: iterable viabegin/end.- Narrow further with
input_range,random_access_range, … when iterator category matters.
Example 3: iterator constraints — [first, last) ranges
For STL-style algorithms, constrain iterators: std::input_iterator vs std::random_access_iterator, etc.
#include <iterator>
template <std::input_iterator It>
void process(It first, It last) {
for (; first != last; ++first) {
// use *first
}
}
Rule of thumb:
- Whole range →
std::ranges::range<R>. - Iterator pair →
std::input_iterator,std::forward_iterator, … - Type family →
std::integral,std::copyable, …
Example 4: generic algorithms — totally ordered types
#include <concepts>
#include <algorithm>
template <std::totally_ordered T>
const T& max_of(const T& a, const T& b) {
return a < b ? b : a;
}
Example 5: data pipelines — statistical mean
#include <ranges>
#include <vector>
#include <numeric>
template <std::ranges::range R>
requires std::arithmetic<std::ranges::range_value_t<R>>
double mean(const R& r) {
auto n = std::ranges::distance(r);
if (n == 0) return 0.0;
using Val = std::ranges::range_value_t<R>;
auto sum = std::accumulate(std::ranges::begin(r), std::ranges::end(r), Val{});
return static_cast<double>(sum) / n;
}
// mean(std::vector<int>{1,2,3,4,5}) → 3.0
std::ranges::range_value_t<R> is the element type; std::arithmetic rejects string containers at compile time.
Example 6: class templates
#include <concepts>
#include <vector>
template <std::copyable T>
class TypedBuffer {
std::vector<T> data_;
public:
void push(const T& value) {
data_.push_back(value);
}
const T& at(size_t i) const { return data_.at(i); }
};
// TypedBuffer<std::unique_ptr<int>> // error: not copyable
// TypedBuffer<int> // OK
std::unique_ptr is movable but not copyable; std::copyable catches misuse early.
7. Common issues and fixes
Issue 1: ambiguous overloads
Symptom: add(3, 5.0) → ambiguous call.
Cause: int matches std::integral, double matches std::floating_point; neither overload is clearly better for mixed arguments.
Fix:
add(3, static_cast<int>(5.0));
// or add a common-type overload:
template <typename T, typename U>
requires std::arithmetic<T> && std::arithmetic<U>
auto add(T a, U b) {
return a + b; // refine return type with std::common_type_t<T,U> if needed
}
Issue 2: “constraints not satisfied” — too strict
Symptom: passing std::vector<int> to something that requires std::integral.
Cause: std::integral applies to scalar integer types, not containers.
Fix:
template <std::ranges::range R>
void process_range(const R& r) {
for (const auto& x : r) { /* ... */ }
}
Issue 3: requires parse errors
Symptom: errors like expected '>' before '&&'.
Cause: complex requires expressions may need parentheses.
Fix:
template <typename T>
requires (std::integral<T> && std::copyable<T>)
void foo(T t) {}
Issue 4: custom Concept — “invalid operands”
Symptom: issues with { a + b } in a requires expression.
Cause: sometimes you must constrain the result of a + b.
Fix:
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
Issue 5: range_value_t errors
Symptom: incomplete type / invalid use.
Cause: R might not be a range, or <ranges> not included.
Fix:
#include <ranges>
template <std::ranges::range R>
void foo(const R& r) {
using value_type = std::ranges::range_value_t<R>;
}
Issue 6: constrained vs unconstrained overload clash
Symptom: both template <typename T> void f(T) and template <std::integral T> void f(T) — ambiguity for f(3).
Fix: remove the unconstrained overload or negate the constraint explicitly:
template <std::integral T>
void f(T t) { /* ... */ }
template <typename T>
requires (!std::integral<T>)
void f(T t) { /* ... */ }
Issue 7: sort on std::list
Symptom: std::ranges::sort(my_list) → not random_access_range.
Cause: sort needs random access; list is bidirectional only.
Fix:
#include <ranges>
#include <list>
#include <vector>
std::list<int> lst = {3, 1, 2};
// std::ranges::sort(lst); // error
std::vector<int> vec(lst.begin(), lst.end());
std::ranges::sort(vec);
// or lst.sort();
Issue 8: return type too strict in requires expression
Symptom: int + double yields double, failing std::same_as<T>.
Fix: relax or drop the return constraint:
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
// or:
template <typename T>
concept Addable = requires(T a, T b) {
a + b;
};
Issue 9: std::movable but copying
Symptom: copy inside template <std::movable T> fails for unique_ptr.
Cause: Movable does not imply copyable.
Fix: use std::copyable when you need copies; std::movable when you only move.
8. Best practices
1) Prefer standard concepts
Reusing std::integral, std::copyable, std::ranges::range, … keeps meaning consistent across a codebase.
template <std::copyable T>
void store(T value);
// Avoid redundant aliases of std::copyable unless you add real semantics.
2) Abbreviated form vs requires clause
One concept → template <Concept T>. Several conditions → requires.
template <std::integral T>
T add(T a, T b) { return a + b; }
template <typename T>
requires std::ranges::range<T> && std::arithmetic<std::ranges::range_value_t<T>>
double mean(const T& r) { /* ... */ }
3) Minimal necessary constraints
Over-constraining hurts usability. If input_range is enough, do not require random_access_range (that would exclude list).
4) Watch overlap between overloads
std::integral and std::floating_point do not overlap; custom concepts might — that produces ambiguity for mixed arguments.
5) Let code be the documentation
Prefer template <std::integral T> over a comment “T must be integral.”
9. Production patterns
Pattern 1: math APIs — sqrt/log vs gcd/lcm
#include <concepts>
#include <cmath>
#include <numeric>
template <std::floating_point T>
T safe_sqrt(T x) {
if (x < 0) throw std::domain_error("sqrt of negative");
return std::sqrt(x);
}
template <std::integral T>
T safe_gcd(T a, T b) {
return std::gcd(a, b);
}
Pattern 2: serialization — custom Concept
See Custom Concepts #22-2.
template <typename T>
concept Serializable = requires(const T& t, std::ostream& os) {
{ t.serialize(os) } -> std::same_as<void>;
};
template <Serializable T>
void save(const T& obj, std::ostream& out) {
obj.serialize(out);
}
Pattern 3: algorithms — ordering
#include <concepts>
#include <algorithm>
template <std::totally_ordered T>
void sort_range(T* first, T* last) {
std::sort(first, last);
}
Pattern 4: C++17/20 dual paths
#if __cplusplus >= 202002L
template <std::integral T>
T add(T a, T b) { return a + b; }
#else
template <typename T>
typename std::enable_if<std::is_integral_v<T>, T>::type
add(T a, T b) { return a + b; }
#endif
Quick selection guide
| Need | Concept | Notes |
|---|---|---|
| Integers only | std::integral | int, long, char, unsigned, … |
| Floats only | std::floating_point | float, double, long double |
| Any arithmetic | std::arithmetic | shared numeric APIs |
| Copyable | std::copyable | storage in containers |
| Move-only OK | std::movable | e.g. unique_ptr |
| Default constructible | std::default_constructible | need T t; |
| Iterable | std::ranges::range | range-for, begin/end |
| Iterator pair | std::input_iterator | classic algorithms |
| Comparable | std::totally_ordered | sort, max, … |
| T → U conversion | std::convertible_to<T, U> | before converting |
10. Performance and compile time
Concepts evaluate constraints at compile time. enable_if relies on SFINAE across overloads; Concepts check constraints first and discard non-viable overloads earlier, which can reduce compile time in large template libraries.
Tips:
- Prefer standard concepts so compilers can use optimized checking paths.
- Too many tiny custom Concepts can multiply overload resolution work — group constraints sensibly.
Related reading
- Custom C++ Concepts — domain-specific constraints [#22-2]
- C++ Concepts and constraints
- C++ metaprogramming evolution: templates to constexpr and reflection
Keywords
C++20 Concepts, concept, template constraints, requires, type constraints.
Summary
| Item | Content |
|---|---|
| Concept | Named conditions on types (checked at compile time) |
| Syntax | template <Concept T> or requires Concept<T> (combine with requires A<T> && B<T>) |
| Standard | std::integral, std::copyable, std::ranges::range, std::input_iterator, … |
| Effects | Clearer errors, cleaner overloads, self-documenting APIs |
| Concepts vs SFINAE | Prefer Concepts in C++20+ for readability and diagnostics |
11. FAQ
Q. When does an error appear if a Concept is not satisfied?
A. At compile time, when the template is selected. The message usually states which Concept failed.
Q. How do I accept both integers and floats?
A.
- Two overloads:
template <std::integral T>andtemplate <std::floating_point T>— homogeneous calls resolve cleanly. - One function for any arithmetic type:
std::arithmetic<T>.
Q. What about domain-specific rules not in the standard?
A. Define custom Concepts with concept Name = requires(...) { ... };. See part #22-2.
Q. Can I stick with template <typename T> only?
A. Yes, it compiles. Wrong types then fail deep inside the template with noisy errors, and intent is unclear. Libraries and public APIs benefit strongly from Concepts.
Q. std::ranges::range vs std::input_iterator?
A. Range: whole container or iterable object. Iterator: pair [first, last). range implies begin/end; input_iterator describes single-pass iteration.
Q. Concepts on C++17 projects?
A. Concepts are C++20. Use #if __cplusplus >= 202002L to offer Concept and enable_if implementations, or migrate the toolchain.
Q. Runtime Concept checks?
A. Concepts are compile-time only. For branching on type properties at compile time, use if constexpr (std::integral<T>) with known T.
Q. Does the standard library use Concepts?
A. Yes — Ranges algorithms (std::ranges::sort, std::ranges::transform, …) are constrained (e.g. random_access_range, indirect_strict_weak_order).
12. Implementation checklist
- Prefer standard concepts when they fit (
std::integral,std::copyable, …) - Single concept → abbreviated
template <Concept T> - Multiple conditions → clear
requires A<T> && B<T> - Watch ambiguous mixed calls with disjoint Concepts — add casts or a common-type overload
- Containers →
std::ranges::range; iterator pairs →std::input_iterator, … - If errors stay long, verify the constraint failure appears first; refine with custom Concepts
See also
- Custom C++ Concepts [#22-2]
- C++20 Coroutines —
co_await/co_yield - C++ Generators — lazy sequences with
co_yield - C++ async and coroutines
- C++ Concepts and constraints