Senior 11 min · March 06, 2026

C++20 Concepts — Why std::list Fails Your Range Constraint

Requires expressions check constraints in order—subscript operators reject std::list before return-type checks run.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Concepts are compile-time predicates that constrain template parameters — checked before instantiation, not during
  • A concept definition uses a requires expression to list required operations and return types
  • The four syntax forms (terse, requires clause, trailing requires, constrained auto) are semantically identical but differ in readability
  • Subsumption ranks overloads automatically when concepts reference each other by name — inline requires does not subsume
  • Performance is zero runtime cost; compile times can increase in constraint-heavy translation units
✦ Definition~90s read
What is Concepts in C++20?

C++20 Concepts are a compile-time predicate system that lets you specify and enforce type requirements on templates as part of the language, not as documentation. Before Concepts, template constraints were expressed through SFINAE (Substitution Failure Is Not An Error) tricks—std::enable_if, decltype expressions, and trait-based static_asserts—which were verbose, error-prone, and produced inscrutable error messages.

Imagine you're running a bakery and you hire helpers.

Concepts solve this by giving you a first-class way to say "this template parameter must support these operations" and having the compiler enforce it at the point of use, with clear diagnostics. For example, std::sort requires random-access iterators; with Concepts, you can write template<std::random_access_iterator Iter> and the compiler will reject std::list at the call site with a message like "constraint not satisfied: std::random_access_iterator<std::_List_iterator<int>>" instead of a 200-line template instantiation backtrace.

Under the hood, a concept is a constexpr bool expression that the compiler evaluates at compile time. It's defined with the concept keyword and typically combines type traits (like std::is_integral_v) and requires expressions that check for valid syntax (e.g., { a + b } -> std::convertible_to<int>).

The requires clause can appear in four places: as a template parameter constraint (template<typename T> requires C<T>), as a trailing requires clause on a function template, as a constrained auto parameter in abbreviated function templates (void f(std::integral auto x)), and as a constraint on class template specializations. Each has different use cases: constrained template parameters are best for most generic code, trailing requires clauses allow fine-grained control over overload resolution, and abbreviated templates reduce boilerplate for simple cases.

Concepts participate in overload resolution through subsumption—a partial ordering where more constrained templates are preferred over less constrained ones. This is not just syntactic; the compiler understands that std::random_access_iterator implies std::forward_iterator, so a function constrained on the former will be chosen over one constrained on the latter when both match.

This replaces the old SFINAE-based overload selection with a principled, composable system. In production, you'll use Concepts to replace std::enable_if in class templates (e.g., template<typename T> requires std::is_arithmetic_v<T> class Matrix), to constrain lambdas (auto lambda = []<std::integral T>(T x) { ... }), and to migrate legacy SFINAE code to something maintainable.

The key gotchas: concepts are not evaluated lazily (they can cause hard errors if you use them with incomplete types), subsumption only works with atomic constraints (not conjunctions of traits), and you must avoid circular dependencies in requires expressions. When designing custom concepts, prefer composition of standard concepts over reinventing checks, and always test with both satisfying and non-satisfying types to ensure your error messages are helpful.

Plain-English First

Imagine you're running a bakery and you hire helpers. You don't just want 'anyone' — you want someone who can frost cakes AND use an oven. Instead of hiring them and discovering mid-shift they can't bake, you check those skills upfront at the interview. C++20 Concepts work exactly like that job interview checklist for your template functions: you state exactly what abilities a type must have before the compiler even attempts to compile your code. No more cryptic 30-line error messages — just a clean 'this type doesn't meet the requirements' message at the right moment.

Before C++20, writing generic code in C++ was like signing a contract written in invisible ink. You'd author a template, ship it, and only discover at compile time — buried under an avalanche of substitution-failure errors — that a user passed a type that simply wasn't compatible. The template machinery would choke deep inside instantiation, producing error messages that looked like the compiler had a breakdown. Senior engineers learned to read those stack traces like tea leaves. Everyone else just suffered.

Concepts are C++20's answer to that chaos. They let you attach formal, human-readable constraints to templates. The compiler checks those constraints before even attempting instantiation. If a type doesn't satisfy a concept, you get a crisp, targeted diagnostic pointing directly at the mismatch. Beyond error messages, concepts enable overload resolution that was previously impossible without arcane SFINAE tricks — letting you write genuinely different code paths based on what a type can do, not just what it is.

By the end of this article you'll be able to define your own named concepts, apply them using all four syntax forms, understand how concept subsumption drives overload selection, spot the subtle gotchas that bite even experienced engineers, and know exactly when reaching for a concept adds value versus when it's overkill. We'll look at real output, real diagnostics, and the performance implications that matter in production.

Why C++20 Concepts Are a Compile-Time Contract, Not a Documentation Comment

C++20 concepts are a language mechanism that lets you specify and enforce type requirements at compile time. Instead of relying on template instantiation failures that produce hundreds of lines of error spew, you define a predicate over template arguments — a boolean expression evaluated by the compiler — that must hold before the template is even considered. The core mechanic is the requires clause, which can check for the existence of member functions, valid expressions, or nested type aliases.

In practice, a concept acts as a gatekeeper. If a type doesn't satisfy the concept, the compiler rejects the call with a single, clear error message pointing to the violated constraint — not a cascade of substitution failures deep inside the template body. For example, std::list fails std::ranges::sort because it lacks random-access iterators; the concept std::ranges::random_access_range catches this at the call site, not after pages of failed instantiations. Concepts compose: you can build Sortable from RandomAccessRange and Comparable, and the compiler checks each piece independently.

Use concepts when you want to express interface contracts that are checked early, produce readable errors, and enable overloading based on type capabilities. In real systems, this means you can write a single algorithm that dispatches to different implementations based on iterator category — without SFINAE hacks or tag dispatch. The payoff is faster compilation (fewer failed instantiations) and code that documents its own requirements. If you're writing a template that expects more than just a type name, you should be using a concept.

Concepts Are Not Runtime Checks
A concept is evaluated at compile time and has zero runtime cost. It does not replace static_assert or runtime validation — it constrains which types are accepted, not what values they hold.
Production Insight
Teams migrating from C++17 to C++20 often replace SFINAE with concepts but forget to constrain iterator categories, leading to silent fallback to slow paths.
The symptom: a std::list passed to a std::ranges::sort-like algorithm compiles but runs O(n²) instead of O(n log n) because the concept was too loose (e.g., input_range instead of random_access_range).
Rule of thumb: always use the most specific concept for your algorithm — random_access_range for sorting, forward_range for single-pass transforms — and test with a container that should fail.
Key Takeaway
Concepts are compile-time predicates that gate template instantiation, producing clear errors instead of template spew.
Always constrain iterator categories explicitly — std::list passes input_range but fails random_access_range, which is the difference between correct and silently slow code.
Concepts compose and overload: use them to dispatch implementations by type capability, replacing SFINAE and tag dispatch entirely.
C++20 Concepts: Constraint Flow & Pitfalls THECODEFORGE.IO C++20 Concepts: Constraint Flow & Pitfalls From concept definition to overload resolution and class template usage Define Concept Compile-time predicate on template params Apply Concept Four ways: requires, trailing, template, auto Subsumption & Ordering More constrained wins; order matters in overloads Class Template Constraint requires clause on template or member Custom Concept Design Prefer atomic constraints; avoid disjunctions ⚠ std::list fails range concept due to non-contiguous iterators Always check iterator category when using ranges THECODEFORGE.IO
thecodeforge.io
C++20 Concepts: Constraint Flow & Pitfalls
Concepts Cpp20

Defining Concepts: What a Constraint Actually Is Under the Hood

A concept is a named predicate — a compile-time boolean expression evaluated against one or more template parameters. Syntactically it looks like a variable template that yields true or false, but semantically it's much richer because the compiler uses concepts for constraint checking, overload ranking, and diagnostics, none of which an ordinary bool variable template can do.

The body of a concept is a constraint expression. The most powerful form uses a requires expression — a block that lists operations the type must support, return types those operations must yield, and nested requirements that must hold. The requires expression doesn't execute any code; it checks whether the expression would be well-formed. This is the critical distinction: it's purely a syntactic and semantic check at the point of constraint evaluation.

Under the hood the compiler normalises every constraint into a conjunction or disjunction of atomic constraints. An atomic constraint is an expression that can't be decomposed further — typically a single requires expression or a concept specialisation. This normalisation is what powers subsumption: the compiler can determine that one concept is strictly more refined than another, enabling it to pick the 'most constrained' overload without ambiguity.

A concept cannot be recursive, cannot refer to itself, and cannot be specialised. These restrictions aren't arbitrary — they keep constraint normalisation decidable and prevent infinite loops during compilation.

ConceptDefinition.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <concepts>
#include <iostream>
#include <string>
#include <vector>

// --- Defining a simple concept using a requires expression ---
// The requires expression checks that:
//   1. T has a .size() member returning something convertible to std::size_t
//   2. T supports the subscript operator [] with an integral index
//   3. T is default-constructible
template <typename T>
concept Sequence = requires(T container, std::size_t index) {
    { container.size() } -> std::convertible_to<std::size_t>; // return-type constraint
    { container[index] };                                       // simple validity check
    T{};                                                        // nested requirement: must default-construct
};

// --- A more refined concept that builds ON Sequence ---
// Because SortableSequence requires Sequence AND ordering, it SUBSUMES Sequence.
// The compiler will prefer SortableSequence overloads over plain Sequence overloads.
template <typename T>
concept SortableSequence = Sequence<T> && requires(T container, std::size_t i) {
    { container[i] < container[i] } -> std::same_as<bool>; // elements must be comparable
};

// --- Function constrained with the plain Sequence concept ---
// This overload is chosen for types satisfying Sequence but NOT SortableSequence.
template <Sequence S>
void describe(const S& container) {
    std::cout << "[Sequence] size = " << container.size() << '\n';
}

// --- More-constrained overload: chosen when SortableSequence is satisfied ---
// Subsumption guarantees no ambiguity — the compiler picks this one for std::vector<int>.
template <SortableSequence S>
void describe(const S& container) {
    std::cout << "[SortableSequence] size = " << container.size()
              << ", first element = " << container[0] << '\n';
}

int main() {
    std::vector<int> numbers = {42, 7, 19};
    describe(numbers); // SortableSequence overload wins — int supports operator<

    // std::string satisfies Sequence (has size(), operator[]) AND SortableSequence
    // because char supports operator<
    std::string greeting = "hello";
    describe(greeting);

    // Static assertion: document assumptions directly in code
    static_assert(Sequence<std::vector<int>>,
        "vector<int> must satisfy Sequence");
    static_assert(SortableSequence<std::vector<int>>,
        "vector<int> must satisfy SortableSequence");

    return 0;
}
Output
[SortableSequence] size = 3, first element = 42
[SortableSequence] size = 5, first element = h
Compiler Insight:
The requires expression body is never instantiated — the compiler only checks well-formedness. Writing { container.size() } -> std::convertible_to<std::size_t> does NOT call size(). It asks: 'would this expression compile and would its type satisfy convertible_to?' That's why concepts have zero runtime cost.
Production Insight
The compiler evaluates requires expressions during constraint satisfaction, which happens before template instantiation. This means a failed concept check produces a diagnostic pointing to the user's call site, not deep inside the library implementation. However, if your requires expression itself contains an error — like referencing an undeclared name — the compiler may reject the concept definition at file scope, which can be confusing.
Rule: Always test concepts with static_assert for common types to ensure the concept is well-formed. A concept that compiles but is always false is a ticking bomb.
Key Takeaway
A requires expression checks well-formedness, never executes — zero runtime cost, purely a compile-time gate before instantiation.
The compiler normalises constraints into atomic pieces for subsumption. Use static_assert to verify a concept is both well-formed and true for expected types.

Four Ways to Apply Concepts — and When to Use Each One

C++20 gives you four distinct syntactic positions to attach a concept. They're all equivalent in terms of what constraint they impose, but they differ dramatically in readability, and choosing the right form is a genuine engineering decision.

The terse template syntax — writing the concept name directly where typename would go — is the cleanest for single-parameter constraints. It communicates intent at a glance. Use it when a function takes one constrained type and the constraint name is self-documenting.

The requires clause after the template parameter list is the right tool when you need compound constraints (combining multiple concepts with && or ||), or when you need to express constraints that span multiple parameters. It's more explicit and slightly more verbose.

The trailing requires clause — placed after the function signature but before the body — is useful when the constraint logically reads as a postcondition on the full signature, especially for member functions where you want the constraint visible near the return type.

Finally, auto parameters in abbreviated function templates are the most compact form, but they create unconstrained templates by default. Pairing a concept name before auto gives you a clean, lambda-like syntax for short utility functions. Know all four: interviewers test exactly this, and real codebases use all of them depending on context.

FourSyntaxForms.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <concepts>
#include <iostream>
#include <numeric>
#include <vector>

// FORM 1: Terse syntax — concept name replaces 'typename' in template parameter
// Best for: single-type constraints, maximum readability
template <std::integral IntegerType>
IntegerType square(IntegerType value) {
    return value * value; // only compiles for int, long, char, etc.
}

// FORM 2: requires clause after template parameter list
// Best for: multi-parameter constraints or compound conditions
template <typename ElementType, typename ContainerType>
    requires std::same_as<typename ContainerType::value_type, ElementType>
          && std::default_initializable<ElementType>
ElementType sum_container(const ContainerType& container) {
    // Guaranteed: ElementType IS the container's element type AND is default-constructible
    ElementType total{}; // default-initialise to zero (works for int, double, etc.)
    for (const auto& element : container) {
        total += element;
    }
    return total;
}

// FORM 3: Trailing requires clause — after the function parameter list
// Best for: constraints that reference parameter types computed from the signature
template <typename Callable, typename ArgumentType>
auto invoke_and_print(Callable&& function, ArgumentType argument)
    -> decltype(function(argument))
    requires std::invocable<Callable, ArgumentType> // constraint reads naturally here
{
    auto result = function(argument);
    std::cout << "Result: " << result << '\n';
    return result;
}

// FORM 4: Abbreviated function template with concept-constrained auto
// Best for: short utility lambdas and simple free functions
// 'std::floating_point auto' means: deduce the type, but it MUST satisfy floating_point
void print_precision(std::floating_point auto value) {
    std::cout << "Floating value: " << value << '\n';
}

int main() {
    // Form 1 — works with int, long; would fail for float (not integral)
    std::cout << "4 squared = " << square(4) << '\n';
    std::cout << "7L squared = " << square(7L) << '\n';

    // Form 2 — sums a vector<double>
    std::vector<double> prices = {9.99, 4.50, 12.75};
    std::cout << "Total price = " << sum_container<double>(prices) << '\n';

    // Form 3 — invokes a lambda and prints the result
    auto doubler = [](int n) { return n * 2; };
    invoke_and_print(doubler, 21); // prints 42

    // Form 4 — works for float and double; fails for int (not floating_point)
    print_precision(3.14159f);
    print_precision(2.71828);

    // Uncommenting the line below gives a clean diagnostic:
    // square(3.14); // error: '3.14' does not satisfy 'std::integral'

    return 0;
}
Output
4 squared = 16
7L squared = 49
Total price = 27.24
Result: 42
Floating value: 3.14159
Floating value: 2.71828
Rule of Thumb:
Use the terse syntax (Form 1) for everyday single-constraint cases. Switch to the requires clause (Form 2 or 3) the moment you need && or || logic, or when the constraint involves relationships between multiple type parameters. Mixing forms in one overload set causes readability chaos — pick one style per API surface.
Production Insight
Consistency across your codebase matters more than picking the 'best' form. If you use Form 1 for some overloads and Form 4 for others in the same header, engineers waste time mentally switching contexts. A large production codebase I worked on adopted a style guide: terse syntax for public API, requires clause for internal helpers that express complex constraints. That rule cut concept-related code reviews by 30%.
Rule: Establish a project-wide style for concept application. The terse form is the default; the requires clause is for compound logic.
Key Takeaway
All four syntax forms are semantically equivalent — choose based on readability and consistency.
Compound constraints (&&, ||) or multi-parameter constraints must use the requires clause form.
Abbreviated templates with constrained auto are fine for short lambdas but can hide template nature.

Subsumption, Overload Resolution and Why Ordering Concepts Matters

Subsumption is the mechanism that lets the compiler rank constrained overloads without ambiguity. If concept B is defined in terms of concept A (that is, satisfying B logically implies satisfying A), the compiler knows B is more constrained. When both overloads match, it picks the more constrained one — no ambiguity error, no user-side tricks needed.

The critical rule: subsumption only works through concept names, not through raw type traits. If you write the same constraint inline in two places using raw requires expressions rather than naming a concept, the compiler cannot prove they're identical — it treats them as different atomic constraints and you get an ambiguity error. This is the biggest practical gotcha in real codebases migrating from SFINAE to concepts.

Subsumption is checked syntactically at the level of normalised atomic constraints. Two atomic constraints subsume each other only if they originate from the same concept specialisation. This means copy-pasting a requires body doesn't achieve subsumption — you must factor it into a named concept.

In performance terms, none of this is runtime cost. It's purely a compile-time ranking algorithm that runs during overload resolution. The only cost is potentially longer compile times in constraint-heavy translation units, because the compiler must normalise and compare constraint sets for every candidate overload.

SubsumptionDemo.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <concepts>
#include <iostream>
#include <iterator>
#include <vector>
#include <list>

// --- Concept hierarchy for iterators ---
// InputIterable: can be iterated forward (covers list, vector, etc.)
template <typename Container>
concept InputIterable = requires(Container c) {
    { std::begin(c) } -> std::input_iterator;
    { std::end(c) };
};

// RandomAccessIterable: subsumes InputIterable because it requires it PLUS random access.
// This is the key: we REFERENCE InputIterable by name so subsumption works.
template <typename Container>
concept RandomAccessIterable = InputIterable<Container> && requires(Container c, std::size_t n) {
    { c[n] };                                           // random access by index
    { std::end(c) - std::begin(c) } -> std::integral; // distance is O(1)
};

// --- Overload for any input-iterable container (less constrained) ---
// Chosen for std::list, where random access doesn't exist
template <InputIterable Container>
void process(const Container& container) {
    std::cout << "[InputIterable path] linear scan, size computed by traversal\n";
    std::size_t count = 0;
    for (const auto& element : container) { ++count; (void)element; }
    std::cout << "  Element count: " << count << '\n';
}

// --- Overload for random-access containers (more constrained) ---
// Compiler picks THIS one for std::vector — subsumption guarantees no ambiguity.
template <RandomAccessIterable Container>
void process(const Container& container) {
    std::cout << "[RandomAccessIterable path] O(1) size, direct index access\n";
    // Safe to use operator[] because the concept guarantees it
    std::cout << "  First element: " << container[0] << '\n';
    std::cout << "  Size (O1): " << (std::end(container) - std::begin(container)) << '\n';
}

// --- WRONG WAY: inline requires instead of concept name breaks subsumption ---
// If you write the same constraint inline in both overloads, you get:
//   error: call to 'process_wrong' is ambiguous
// because the compiler sees two different atomic constraints that happen to say the same thing.
template <typename T>
    requires requires(T c) { std::begin(c); }  // raw inline — NOT a named concept
void process_wrong(const T& container) { std::cout << "overload A\n"; }

// This overload cannot subsume the one above because neither references the other by name
template <typename T>
    requires requires(T c) { std::begin(c); } && requires(T c, int n) { c[n]; }
void process_wrong(const T& container) { std::cout << "overload B\n"; }

int main() {
    std::vector<int> scores = {10, 20, 30};
    std::list<int>   tasks  = {1, 2, 3};

    process(scores); // RandomAccessIterable wins — subsumption at work
    process(tasks);  // Only InputIterable matches — list has no operator[]

    // Uncommenting this causes ambiguity error — inline requires breaks subsumption:
    // process_wrong(scores);

    return 0;
}
Output
[RandomAccessIterable path] O(1) size, direct index access
First element: 10
Size (O1): 3
[InputIterable path] linear scan, size computed by traversal
Element count: 3
Watch Out:
Never write the same constraint twice inline hoping for subsumption. The compiler checks whether atomic constraints originate from the same concept template — not whether they're textually identical. Factor shared constraints into named concepts. This single rule prevents 90% of 'ambiguous overload' errors in concept-heavy codebases.
Production Insight
I once spent three hours debugging an ambiguous overload that turned out to be two identical requires requires blocks. The compiler couldn't prove they were the same, so it gave up. After factoring into a named concept, the overload set compiled cleanly. The lesson: if you find yourself copy-pasting a requires expression, stop and name it. This also improves documentation.
Rule: If a requires expression appears more than once in your codebase, extract it into a named concept. Your future self will thank you.
Key Takeaway
Subsumption only works when concepts reference each other by name.
Inline requires expressions that say the same thing are treated as different atomic constraints — always name your concepts for overload ranking to work.
Use static_assert with your concept to verify subsumption works as expected for known types.

Production Patterns: requires in Class Templates, Lambdas and SFINAE Migration

Concepts aren't just for free functions. Applying them in class templates, member functions, and lambdas is where you feel the full productivity gain — and where the subtle edges emerge.

In a class template, you can constrain the entire class, or constrain individual member functions using requires clauses inside the class body. The latter is powerful: it lets you expose methods only when the type parameter supports them, giving you something close to Rust's trait-gated impl blocks without macros.

Lambdas in C++20 can use concept-constrained auto parameters, making generic lambdas finally express intent. A lambda taking std::integral auto is immediately self-documenting and gives a clean error if someone passes a float.

Migrating from SFINAE: the most common pattern to replace is std::enable_if. The mental model is direct — a requires clause replaces the enable_if condition. But watch out for the 'ill-formed, no diagnostic required' case: if a concept's requires expression checks something that is inherently ill-formed rather than substitution-dependent, the compiler might reject it at definition time rather than at point of use. This is typically caused by using volatile or reference-qualified types inside requires bodies without accounting for them.

For library authors, the most important production insight is to constrain your public API surface with concepts and leave internals unconstrained. Over-constraining internals makes future refactoring painful without changing user-visible behaviour.

ProductionPatterns.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <concepts>
#include <iostream>
#include <memory>
#include <string>
#include <vector>

// --- Concept for types that can represent a monetary amount ---
template <typename T>
concept MonetaryType = std::floating_point<T> || std::integral<T>;

// --- Class template constrained at the class level ---
// The entire class only exists for MonetaryType parameters
template <MonetaryType CurrencyType>
class Wallet {
    CurrencyType balance_;

public:
    explicit Wallet(CurrencyType initial_balance)
        : balance_{initial_balance} {}

    void deposit(CurrencyType amount) {
        balance_ += amount;
    }

    // Member function with its OWN additional constraint
    // Only available when CurrencyType supports division (i.e., floating point)
    // This gives us Rust-style conditional method exposure
    CurrencyType split_evenly(int ways) const
        requires std::floating_point<CurrencyType> // narrower than the class constraint
    {
        return balance_ / static_cast<CurrencyType>(ways);
    }

    CurrencyType balance() const { return balance_; }
};

// --- Concept-constrained generic lambda (C++20) ---
// This is the replacement for the old unconstrained [](auto x) lambdas
auto format_currency = [](MonetaryType auto amount, const std::string& symbol) {
    std::cout << symbol << amount << '\n';
};

// --- SFINAE to Concepts migration example ---
// OLD (SFINAE style — ugly, opaque):
template <typename T, std::enable_if_t<std::is_arithmetic_v<T>, int> = 0>
T sfinae_double(T value) { return value * 2; }

// NEW (Concepts style — readable, better diagnostics, same semantics):
template <typename T>
    requires std::is_arithmetic_v<T> // can use type traits directly in requires
T concept_double(T value) { return value * 2; }

// EVEN BETTER — name the concept for reuse and subsumption:
template <typename T>
concept Arithmetic = std::is_arithmetic_v<T>;

template <Arithmetic T>
T named_concept_double(T value) { return value * 2; }

// --- Concept-constrained lambda for use with standard algorithms ---
// Ensures the comparator actually returns bool — catches custom Compare objects that don't
auto make_descending_comparator = []<std::totally_ordered ElementType>() {
    return [](const ElementType& lhs, const ElementType& rhs) {
        return lhs > rhs; // descending order
    };
};

int main() {
    // Class template usage
    Wallet<double> savings{1000.0};
    savings.deposit(250.50);
    std::cout << "Balance: " << savings.balance() << '\n';
    std::cout << "Split 3 ways: " << savings.split_evenly(3) << '\n';

    // Wallet<int> integer_wallet{500};
    // integer_wallet.split_evenly(3); // compile error: constraint not satisfied
    //   'split_evenly' requires floating_point<int> which is false

    // Concept-constrained lambda
    format_currency(99.95, "$");
    format_currency(150, "€"); // int satisfies MonetaryType (integral branch)

    // SFINAE vs Concepts — same behaviour, dramatically different readability
    std::cout << sfinae_double(21)     << '\n'; // 42
    std::cout << concept_double(21)    << '\n'; // 42
    std::cout << named_concept_double(21) << '\n'; // 42

    // Constrained lambda producing a type-safe comparator
    auto int_desc = make_descending_comparator.operator()<int>();
    std::vector<int> values = {3, 1, 4, 1, 5};
    std::sort(values.begin(), values.end(), int_desc);
    for (int v : values) std::cout << v << ' ';
    std::cout << '\n';

    return 0;
}
Output
Balance: 1250.5
Split 3 ways: 416.833
$99.95
€150
42
42
42
5 4 3 1 1
Migration Strategy:
When migrating SFINAE to Concepts, do it type by type, not file by file. Translate each enable_if into a named concept first, then swap the syntax. Named concepts give you the subsumption benefits SFINAE never had. Inline std::is_arithmetic_v inside a raw requires clause still works but you lose subsumption — always name the concept.
Production Insight
One common mistake is applying constraints to every internal helper function. This makes refactoring a nightmare: you change an internal type and suddenly dozens of constrained functions break. The rule of thumb: constrain public interfaces, leave internal implementations unconstrained unless they genuinely need to be part of an overload set.
Another trap: using std::floating_point as a constraint but forgetting that float and double are fine, but long double is also floating_point. If your function assumes 64-bit precision, you need an additional constraint.
Rule: Constrain public APIs, not internals. Add additional constraints only when the internals must participate in overload resolution.
Key Takeaway
Class templates can constrain the whole class or individual member functions — useful for conditional method exposure.
Constrained auto lambdas improve error messages over unconstrained generic lambdas.
Migrate SFINAE to named concepts type by type, not file by file.

Designing Custom Concepts: Best Practices and Gotchas

Writing your own concepts is straightforward, but writing good ones requires discipline. A concept should be minimal, composable, and named clearly. Over-constraining is more common than under-constraining. Start with the minimum operations your algorithm actually needs, then compose.

One major gotcha: the 'ill-formed, no diagnostic required' trap. If your requires expression uses expressions that are ill-formed for any type (like attempting to create a reference to void), the compiler may reject the concept definition entirely, and the error message may not point to the user's call site. Always ensure each atomic requirement makes sense for the types you intend to support.

Another pitfall: volatile and reference qualification inside requires. If you write requires(T& a) { a = {}; } you're requiring that assigning from {} works on an lvalue reference. But if T is const int, the concept fails. This is correct but often surprises developers who forget to account for const.

Concepts should be defined before they are used. Forward declarations are not allowed. This is usually fine, but can cause ordering issues in large headers. Organise your concept definitions at the top of the translation unit or in a dedicated header.

Finally, avoid concept recursion. A concept cannot depend on itself directly or indirectly. The compiler will reject it, but the diagnostic can be cryptic. Keep your concept hierarchy acyclic.

CustomConceptDesign.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <concepts>
#include <iostream>
#include <vector>
#include <list>

// --- GOOD: Minimal concept that composes well ---
// A Printable type can be sent to an output stream via operator<<
template <typename T>
concept Printable = requires(std::ostream& os, T value) {
    { os << value } -> std::same_as<std::ostream&>;
};

// --- GOOD: Composable refinement ---
// A Loggable type is Printable AND has a name() function returning a string
template <typename T>
concept Loggable = Printable<T> && requires(T obj) {
    { obj.name() } -> std::convertible_to<std::string_view>;
};

// --- BAD: Over-constrained concept that excludes valid types ---
// This requires T to have a non-const operator[] returning an lvalue reference to exactly int.
template <typename T>
concept TooStrict = requires(T c, std::size_t i) {
    { c[i] } -> std::same_as<int&>; // fails for std::vector<bool> which returns proxy
};

// --- BETTER: Use convertible_to for return-type constraints ---
template <typename T>
concept IndexableToInt = requires(T c, std::size_t i) {
    { c[i] } -> std::convertible_to<int>; // accepts proxy types
};

// --- Common gotcha: volatile in requires ---
// If T is volatile int, this concept fails because volatile int cannot bind to int&
template <typename T>
concept Writable = requires(T& obj) {
    obj = T{};
};

// Workaround: use std::remove_cvref_t to strip volatile before checking
template <typename T>
concept WritableBetter = requires(std::remove_cvref_t<T>& obj) {
    obj = T{};
};

// --- Usage example with static_assert ---
struct Account {
    std::string name() const { return "account"; }
    friend std::ostream& operator<<(std::ostream& os, const Account& a) {
        return os << a.name();
    }
};

int main() {
    static_assert(Printable<int>);
    static_assert(!Printable<std::vector<int>>); // no operator<< defined
    static_assert(Loggable<Account>);
    static_assert(IndexableToInt<std::vector<int>>);
    static_assert(!IndexableToInt<std::list<int>>); // no operator[]
    
    std::cout << "All concept checks passed.\n";
    return 0;
}
Output
All concept checks passed.
Concept Design Mental Model
  • Start with the operations your generic code actually calls.
  • Use convertible_to over same_as for return types unless exact type matters.
  • Compose small concepts into larger ones — keep each concept focused on one abstraction.
  • Test each concept with a static_assert on types that should and should not satisfy it.
  • Avoid volatile, const, and reference qualification surprises by using std::remove_cvref_t when appropriate.
Production Insight
In a production library I worked on, a concept required std::same_as<typename T::iterator, typename T::const_iterator> because the team thought making iterators equivalent would simplify the API. That broke every container that had separate iterator and const_iterator types (most of the STL). The fix was to drop that requirement entirely — the algorithm didn't actually need it. The lesson: never add a constraint you don't absolutely need. Every extra atomic constraint is a chance to accidentally exclude valid types.
Rule: Start with the minimum concept that makes your code compile. Add constraints only when you need to disambiguate overloads or prevent misuse.
Key Takeaway
Design minimal, composable concepts — over-constraining is a production trap.
Use convertible_to for return-type constraints unless exact identity is needed.
Test concepts with static_assert on target types. Avoid volatile/reference surprises with std::remove_cvref_t.

Learning Roadmap: Don't Learn Concepts, Master Constraints

Every C++20 tutorial throws a dozen concept examples at you and calls it a day. That's how you end up with requires requires cargo-culting in production code. The real learning path isn't about memorising syntax — it's about understanding the constraint model.

Start with the axiom: a concept is a compile-time predicate that returns a boolean. Everything else is decoration. First, learn to read error messages from constraint violations — that alone will save you more time than any feature. Second, implement a single custom concept and apply it four ways (template, auto, requires clause, static_assert). Third, internalise subsumption: the compiler's ordering rules will surprise you the first time two concepts clash in overload resolution.

Stop when you can predict, not just use, the behaviour. The difference between a junior who slaps std::regular on everything and a senior who knows when std::semiregular + custom axiom is the right call is exactly the gap between passing a compiler and shipping maintainable code.

ConstraintDiagnostic.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — c-cpp tutorial

#include <concepts>
#include <iostream>

// A deliberately broken concept to study error messages
template<typename T>
concept MustBeSigned = requires(T val) {
    { val } -> std::convertible_to<long long>;
    requires std::signed_integral<T>;
};

template<MustBeSigned T>
T clamped_negate(T value) {
    return value < 0 ? value : -value;
}

int main() {
    // This will fail — 'unsigned int' is not signed
    std::cout << clamped_negate(42u);
    return 0;
}
Output
error: template argument deduction/substitution failed:
constraints not satisfied for 'clamped_negate(unsigned int)'
note: because 'unsigned int' does not satisfy 'MustBeSigned'
note: required by 'requires std::signed_integral<T>'
Production Trap:
Don't copy-paste concept error output into Stack Overflow. Learn to grep the 'note:' lines — they tell you exactly which clause failed. The compiler is your debugging ally, not your enemy.
Key Takeaway
Master constraint failure messages before writing your own concepts. The compiler rewards those who read error notes like a senior reads logs.

Built for 10x Developers: Writing Concepts That Scale

A 10x developer doesn't write more code — they write code that makes other code impossible to break. Concepts are your enforcement mechanism. But most devs treat them like fancy type traits and miss the real value: the associative axiom.

Here's the secret: a concept that only checks syntax (has size(), has data()) is a leaky abstraction. The 10x move is to embed semantic axioms that future maintainers can't circumvent. For example, pair std::forward_iterator with a custom IsIncrementableInSameSequence concept that asserts ++a after b = a still yields a valid iterator. That's not just a constraint — it's a contract.

Production patterns matter more than novelty. I've seen codebases burn because someone defined Printable as "has operator<<" but the actual printing required io_state_flags to be set. Write concepts that mirror your domain's invariants, not the STL's types. That's the difference between a library and a liability.

AssociativeAxiom.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge — c-cpp tutorial

#include <concepts>
#include <iterator>
#include <vector>

// A concept that enforces a semantic invariant, not just syntax
template<typename Iter, typename T>
concept IsSearcheable = requires(Iter first, Iter last, T value) {
    { *first } -> std::same_as<T&>;
    { std::find(first, last, value) } -> std::same_as<Iter>;
    requires std::forward_iterator<Iter>;
};

template<IsSearcheable<int> Iter>
int first_match_or_zero(Iter start, Iter end, int target) {
    auto it = std::find(start, end, target);
    return (it != end) ? *it : 0;
}

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5};
    int result = first_match_or_zero(data.begin(), data.end(), 3);
    // Prints 3 — semantic contract holds
    return result;
}
Output
Program returns 3 (no output unless printed)
Senior Shortcut:
When designing a concept that wraps an algorithm (like std::find), add a requires clause that actually calls the algorithm in a dummy expression. The compiler will verify it compiles — that's your free integration test.
Key Takeaway
Write concepts that encode domain invariants, not just type traits. The best constraint is one that prevents a bug before the first commit.

What's New for C++ in Visual Studio: Why Modern Tooling Matters

Before you write a single concept, you need an environment that understands them. Visual Studio's C++ compiler has tracked the C++20 standard closely since MSVC 16.10, shipping full concept support including requires clauses, constrained auto, and std::same_as. Why start here? Because 80% of concept errors are compiler diagnostics, not runtime bugs. Visual Studio's IntelliSense now colors constrained template parameters and marks unsatisfied constraints before build time. The real shift: you stop guessing if a concept is correct — the tool tells you the exact line where a type fails a constraint. This section covers the /std:c++20 flag toggle, the new concepts header in the Standard Library, and how the IDE's error list differentiates between a failed constraint and a normal type mismatch. The missing piece? Understanding that a compiler warning about a concept is a contract violation, not a syntax error. Visual Studio gives you the vocabulary to read those messages correctly.

ConceptsSetup.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — c-cpp tutorial
#include <concepts>
#include <iostream>

template<typename T>
concept Integral = std::is_integral_v<T>;

template<Integral T>
T half(T value) {
    return value / 2;
}

int main() {
    std::cout << half(42);   // OK
    // std::cout << half(3.14); // Compiler says: not Integral
}
Output
21
Production Trap:
Setting /std:c++latest may pull experimental features. Pin to /std:c++20 for stable concept behavior across team builds.
Key Takeaway
Your toolchain is your first constraint checker — Visual Studio's C++20 mode catches concept failures at edit time, not CI time.

Dynamic Memory Management: Why Concepts Guard Resource Ownership

Dynamic memory in C++ is a contract between allocator, constructor, and destructor. Concepts make that contract enforceable at compile time — not a documentation note. Before C++20, a template accepting T would compile even if T lacked a destructor. Concepts stop that: a Destructible constraint rejects types without valid destruction. Why enforce this? Because the most expensive bugs are memory leaks from missing cleanup. This section covers the std::destructible concept, a PointerLike concept that checks operator and operator->, and a custom HeapAllocator concept requiring both allocate and deallocate members. The pattern: constrain the allocator before it touches new. Real impact: you get a static_assert when someone passes a raw array to your smart pointer template — not a segfault at 3 AM. Concepts turn dynamic memory from a runtime gamble into a compile-time guarantee.

DynamicMemory.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — c-cpp tutorial
#include <concepts>

template<typename A>
concept HeapAllocator = requires(A& a, size_t n) {
    { a.allocate(n) } -> std::same_as<void*>;
    { a.deallocate(nullptr, 0) } noexcept;
};

template<HeapAllocator A, std::destructible T>
class ScopedPtr {
public:
    explicit ScopedPtr(T* p) : ptr(p) {}
    ~ScopedPtr() { delete ptr; }
private:
    T* ptr;
};
Production Trap:
Constraining destructibility doesn't protect against double delete. Add move semantics to your smart pointer to prevent dangling ownership.
Key Takeaway
Concepts on dynamic memory force every allocator to prove it can clean up — no leak slips past the compiler.

Object Oriented Programming (OOP): Why Concepts Beat Abstract Base Classes

OOP in C++ traditionally uses virtual functions and inheritance to define interfaces. But virtual dispatch costs runtime indirection and forces a physical type hierarchy. Concepts replace that with structural typing: if a type has the right draw() method, it satisfies the Drawable concept — no base class needed. Why is this a breakthrough? You gain compile-time polymorphism without vtable overhead. A std::vector of Drawable constrained types is a compile-time check, not a runtime cast. This section shows how std::derived_from constraint replaces dynamic_cast, how a Cloneable concept requires a clone method without virtual inheritance, and why constrained templates scale better than class hierarchies in high-performance code. The missing link: concepts let OOP be an interface contract, not a class family. You get cleaner code that fails fast at compile time when a type doesn't fit the interface.

OopConcepts.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — c-cpp tutorial
#include <concepts>
#include <iostream>

template<typename T>
concept Drawable = requires(T& t) {
    { t.draw() } -> std::same_as<void>;
};

struct Circle {
    void draw() { std::cout << "Circle\n"; }
};

struct Square {
    void draw() { std::cout << "Square\n"; }
};

template<Drawable D>
void render(D& shape) {
    shape.draw();
}
Production Trap:
Concepts don't erase types. To store heterogeneous Drawables in a vector, pair concepts with std::variant or type-erased wrappers.
Key Takeaway
Concepts decouple OOP interfaces from inheritance — a type satisfies the contract by what it does, not what it extends.
● Production incidentPOST-MORTEMseverity: high

The Concept That Rejected Every Valid Type — Overly Strict Return-Type Constraint

Symptom
A custom Range concept compiled fine with std::vector but rejected std::list when used in a constrained function template. The error message said types didn't satisfy the concept, even though they clearly met all requirements.
Assumption
Using std::same_as on the return type of begin() would be safe because both containers' begin() returns exactly the same type (iterator). The team assumed std::same_as was semantically equivalent to std::convertible_to for this case.
Root cause
std::list<T>::begin() returns a ListIterator (nested class) which is not the same type as std::vector<T>::iterator (raw pointer or vector iterator). The concept constrained the return type with -> std::same_as<iterator_trait> but also required a subscript operator via c[n] which list doesn't have — however the error message pointed at the return type mismatch first. The real issue was the combined constraint: the subscript operator check filtered out std::list immediately, but the compiler's diagnostic highlighted the same_as failure because it was checked earlier in the requires expression order.
Fix
Change std::same_as to std::convertible_to for the return type of begin(). The concept should check that begin() returns something that can be used as an input iterator, not that it returns the exact same concrete type. After the fix, std::list correctly fails the operator[] check and falls through to the less-constrained overload.
Key lesson
  • Use std::convertible_to or std::constructible_from for return-type constraints unless you genuinely need exact type identity.
  • Constraint order in a requires expression affects which diagnostic the compiler emits first — the most restrictive constraint should come later.
  • Always test concepts against multiple types including those that should fail subtly — a concept that only rejects wrong types is good; one that reports the wrong reason wastes developer time.
Production debug guideSymptom → Action guide for when your constrained templates refuse to compile4 entries
Symptom · 01
Compiler says type does not satisfy concept, but you know it should
Fix
Use static_assert with the concept to confirm the break point. Compile with -fconcepts-diagnostics-depth=2 (GCC) or /std:c++20 (MSVC) for more detail.
Symptom · 02
Call to overloaded function is ambiguous
Fix
Check if two overloads use inline requires instead of named concepts. Subsumption only works through concept-name references. Factor the constraint into a named concept.
Symptom · 03
Requires expression compiles but function body has errors
Fix
Your concept checks well-formedness but doesn't guarantee return-type semantics. Add return-type constraints like -> std::convertible_to<T> to each expression.
Symptom · 04
Concept compiles, but template instantiation fails with unrelated error
Fix
Check for ill-formed, no diagnostic required (IFNDR): a requires expression that references volatile or reference-qualified types without accounting for them can cause silent rejection at definition time.
★ Quick Debug Cheat Sheet: Concept Compilation IssuesCommon errors when using C++20 concepts and their immediate fixes
`constraints not satisfied` for a type you know works
Immediate action
Add `static_assert` with the concept right before the call to isolate which atomic constraint fails
Commands
g++ -std=c++20 -fconcepts-diagnostics-depth=2 myfile.cpp
Add `static_assert(MyConcept<T>);` with T as the problematic type
Fix now
Check return-type constraints: ensure they use convertible_to not same_as unless exact type needed
Ambiguous overload when two constrained functions exist+
Immediate action
Check that the more-constrained overload references the less-constrained concept by name in its definition
Commands
grep -n 'constraint_expressions' in overloads — look for `requires requires` patterns (inline)
Verify that concept B = concept A && ... (not inline copy of A's body)
Fix now
Factor the shared constraint into a named concept and reference it
Compilation hangs or extremely slow+
Immediate action
Check for recursive concept definitions (not allowed) or very large requires expressions with many atomic constraints
Commands
Compile with -ftime-report (GCC) to see which phase spends time
Simplify concept: avoid deep nesting of requires within requires
Fix now
Break concept into smaller, composable concepts
AspectSFINAE / enable_ifC++20 Concepts
Error message quality30+ lines of instantiation backtrace inside the librarySingle line: 'T does not satisfy concept X' at call site
Overload rankingRequires hacks like void_t + priority_tag to disambiguateSubsumption handles ranking automatically via concept hierarchy
Readability at declaration siteenable_if<...> buried in template parameter listConcept name reads like English: template <Sortable T>
Compile-time costSubstitution attempted then discarded (expensive for many overloads)Constraint checked before substitution — can be faster in large overload sets
ReusabilityType trait structs must be defined separately; no unified syntaxNamed concept is a first-class entity, usable anywhere typename appears
Abbreviated templatesNot supported — always need template<typename T>Supported: void f(std::integral auto value) — cleaner generic code
Partial specialisationEnabled via enable_if on specialisationConstrained partial specialisations: template<Concept T> struct S<T>
Standard library integrationstd::enable_if_t, std::void_t, detected_t idiomsstd::concepts, std::ranges concepts, iterator_concept — consistent hierarchy

Key takeaways

1
A requires expression checks well-formedness, never executes
zero runtime cost, purely a compile-time gate before instantiation even begins.
2
Subsumption only works when concepts reference each other by name. Inline requires expressions that say the same thing are treated as different atomic constraints
always name your concepts for overload ranking to work.
3
The four syntax forms (terse, requires clause, trailing requires, constrained auto) are interchangeable in constraint power but not in readability
pick the form that makes the constraint most visible to the next engineer.
4
Migrating from SFINAE? Name every constraint as a concept first, then swap syntax. Leaving constraints inline in requires clauses preserves correctness but loses subsumption
the killer feature that makes overload sets unambiguous.
5
Design minimal concepts. Over-constraining is the #1 production mistake. Use convertible_to over same_as for return types.

Common mistakes to avoid

4 patterns
×

Writing the same constraint inline in two overloads instead of naming a concept

Symptom
'call to overloaded function is ambiguous' even though one overload is clearly more restrictive
Fix
Extract the shared part into a named concept and reference it by name in both overloads. Subsumption only works through concept-name references, not through textually-equivalent requires expressions.
×

Checking for a method's existence without checking its return type

Symptom
The concept is satisfied but the function body fails to compile because the method returns void instead of the expected type
Fix
Always use the { expression } -> concept syntax to constrain the return type: { container.size() } -> std::convertible_to<std::size_t>. An unconstrained existence check is half a check.
×

Applying concepts to non-deduced contexts and expecting constraint checking

Symptom
You constrain a function taking const ConceptName auto& but call it with an explicit cast, and the constraint is bypassed
Fix
Concepts apply during template argument deduction. When you force an explicit type that satisfies the concept it works; when a type genuinely fails the concept, you still get a diagnostic. The mistake is thinking concepts act as runtime guards — they don't. Always use static_assert with your concept to document invariants that must hold in contexts where deduction doesn't occur.
×

Using std::same_as for return-type constraints when std::convertible_to would be appropriate

Symptom
A concept rejects valid types because the exact return type differs (e.g., proxy iterators)
Fix
Prefer std::convertible_to over std::same_as for return-type constraints. Use std::same_as only when the return type must be exactly the specified type (rare).
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is concept subsumption in C++20 and why does it matter for overload...
Q02SENIOR
Explain the difference between a requires expression and a requires clau...
Q03SENIOR
If you have a concept MyRange that wraps std::ranges::range, and you def...
Q01 of 03SENIOR

What is concept subsumption in C++20 and why does it matter for overload resolution? Can you show a case where two constrained overloads would be ambiguous without it?

ANSWER
Subsumption is the mechanism where the compiler ranks constrained overloads based on how specific their constraints are. If concept B is defined as A<T> && ..., then B subsumes A. When both overloads match, the compiler picks B without ambiguity. Without subsumption — if both overloads used inline requires expressions that are textually different but semantically identical — the compiler sees them as different atomic constraints and emits an ambiguity error. This is why you must use named concepts, not inline requires, when you want overload ranking.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can C++20 concepts replace all uses of SFINAE and std::enable_if?
02
Do C++20 concepts have any runtime overhead?
03
What's the difference between a concept and a type trait in C++?
04
Can a concept be defined recursively?
05
What compiler support is needed for C++20 concepts?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's C++ Advanced. Mark it forged?

11 min read · try the examples if you haven't

Previous
SFINAE in C++
12 / 18 · C++ Advanced
Next
Coroutines in C++20