C++20 Concepts | Making Template Error Messages Readable

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
AspectSFINAE / enable_ifConcepts (C++20)
ReadabilityVerbose, e.g. typename std::enable_if<std::is_integral_v<T>>::type* = nullptrtemplate <std::integral T> states intent in one line
Errors“Substitution failed” or long stacksMessages like “T does not satisfy std::integral”
Composition&& / || combinations get very heavyrequires A<T> && B<T> stays readable
OverloadsPossible but verboseClean 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<...>::type on 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 requires syntax.
  • 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

  1. Real-world problem scenarios
  2. What is a Concept?
  3. The requires clause
  4. Standard concepts — full example
  5. Applying to function templates
  6. Practical use
  7. Common issues and fixes
  8. Best practices
  9. Production patterns
  10. Performance and compile time
  11. Summary and FAQ
  12. Implementation checklist

1. Real-world problem scenarios

Typical situations without Concepts:

SituationSymptomAfter Concepts
Wrong type passed50–100 line instantiation stackOne line: “does not satisfy std::integral”
Unclear API intentHard to see allowed types from codetemplate <std::copyable T> makes it obvious
Complex overload rulesMany enable_ifs — hard to traceClear separation per Concept
Iterator vs range confusionCryptic errors applying sort to listImmediate “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 vector yields “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?

  • square could take integers and floats, but constraining to integers makes “integer-only” explicit and simplifies a later floating-point overload if you add one.
  • half takes floats because for integers, half(3) is integer division (3/21), while half(3.0) is 1.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 a requires clause.
  • A requires clause follows template <typename T> on the next line: requires condition.

3. The requires clause

requires is used in two ways:

  1. requires clause: combine existing Concepts or predicates — “this template is only valid when…”
  2. requires expression: define new Concepts by requiring certain operations or members on T.

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, doubleAddable 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

ConceptMeaningWhen to use
std::integral<T>Integer typesNumeric APIs that must be integers
std::floating_point<T>float / double / long doubleFloating-point-only APIs
std::copyable<T>Copy construct/assignTypes stored in vector<T>, etc.
std::movable<T>Move construct/assignMove-only or move-preferred APIs
std::default_constructible<T>Default constructionNeed T t;
std::same_as<T, U>T and U are the same typeTwo template parameters must match
std::convertible_to<T, U>T converts to UBefore implicit conversions
std::arithmetic<T>Integer or floating-pointGeneric 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 via std::integral.
  • reciprocal: division rules via std::floating_point.
  • safe_multiply: requires for “integral and ≥4 bytes.”
  • push_twice: std::copyable for push_back.
  • print_range: std::ranges::range for vector, 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::integral or std::floating_point.
  • std::copyable: includes std::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"]
ConceptMeaningExample
input_rangeForward read oncestd::istream_view
forward_rangeMulti-passstd::forward_list
bidirectional_rangePrev/nextstd::list
random_access_rangeIndexingstd::vector, std::deque
contiguous_rangeContiguous storagestd::vector, C arrays

Choosing:

  • “Just iterate” → std::ranges::range or input_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 via begin/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

NeedConceptNotes
Integers onlystd::integralint, long, char, unsigned, …
Floats onlystd::floating_pointfloat, double, long double
Any arithmeticstd::arithmeticshared numeric APIs
Copyablestd::copyablestorage in containers
Move-only OKstd::movablee.g. unique_ptr
Default constructiblestd::default_constructibleneed T t;
Iterablestd::ranges::rangerange-for, begin/end
Iterator pairstd::input_iteratorclassic algorithms
Comparablestd::totally_orderedsort, max, …
T → U conversionstd::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.

  • 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

ItemContent
ConceptNamed conditions on types (checked at compile time)
Syntaxtemplate <Concept T> or requires Concept<T> (combine with requires A<T> && B<T>)
Standardstd::integral, std::copyable, std::ranges::range, std::input_iterator, …
EffectsClearer errors, cleaner overloads, self-documenting APIs
Concepts vs SFINAEPrefer 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> and template <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