Concepts constrain template parameters with compile-time predicates — a contract for types, not runtime checks.
Ranges compose lazy transformations over sequences without allocating intermediate containers.
Coroutines enable cooperative multitasking: functions that suspend/resume without blocking threads.
Modules replace header files with a proper compilation boundary; no more #include order bugs.
The three-way comparison operator (<=>) auto-generates all relational operators for a class.
Biggest gotcha: coroutines allocate heap frames by default; not every function should be async.
✦ Definition~90s read
What is C++20 Features?
C++20 is the most significant revision to the C++ standard since C++11, introducing four major language features that fundamentally change how you write performant, expressive code. Concepts allow you to specify compile-time constraints on template parameters, replacing SFINAE tricks with clear, readable contracts that produce sane error messages.
★
Imagine you're a chef in a restaurant kitchen.
Ranges provide a composable, lazy-evaluation pipeline for algorithms (e.g., views::filter | views::transform), eliminating manual iterator loops and enabling zero-overhead abstractions for data processing. Coroutines introduce stackless, resumable functions (co_await, co_yield, co_return) that let you write asynchronous I/O and generator patterns without explicit state machines or thread pools—though, as the article's crash illustrates, their lifetime and frame management require careful handling.
Modules finally replace the preprocessor-based #include system with proper compilation boundaries, drastically reducing build times and encapsulation leaks. The spaceship operator <=> auto-generates all six comparison operators from a single definition, eliminating boilerplate for ordered types.
These features are not syntactic sugar: they change how you reason about resource lifetimes, compilation units, and template metaprogramming. If you're maintaining a C++17 codebase, you'll want to adopt Concepts and Ranges first for immediate safety and clarity gains; Modules require build system changes (CMake 3.28+, MSVC, Clang 17+), and Coroutines demand deep understanding of the promise type protocol to avoid stack corruption or dangling frames.
Plain-English First
Imagine you're a chef in a restaurant kitchen. Before C++20, you could hire any cook and hope they knew how to handle a knife — and only find out they didn't when dinner service collapsed. C++20 Concepts are like requiring a formal knife-skills certificate before they step foot in your kitchen. Ranges are like having a smart sous chef who preps ingredients in exactly the order you need them, on demand, instead of dumping everything on the counter at once. Coroutines are like a cook who can pause mid-recipe, hand the stove to someone else, and pick up exactly where they left off — no wasted effort, no lost progress.
C++20 is the biggest revision to the language since C++11 rewired how we think about modern C++. It didn't just add syntax sugar — it introduced four pillars that fundamentally change how you architect, template, and scale C++ codebases: Concepts, Ranges, Coroutines, and Modules. These aren't academic curiosities. Google, Microsoft, and JetBrains are already shipping production code that leans on these features, and the compilers — GCC 10+, Clang 10+, MSVC 19.28+ — have solid enough support that there's no excuse for ignoring them in greenfield projects.
Before C++20, template error messages were infamous horror shows — pages of substitution failures that pointed nowhere useful. Constraints on template parameters were enforced via SFINAE tricks that even seasoned engineers would copy-paste without fully understanding. Lazy data pipelines required external libraries like range-v3. Asynchronous code meant either raw threads, callback hell, or heavy framework dependencies. Every one of these pain points has a direct answer in C++20.
By the end of this article you'll understand not just what each feature does, but why it was designed that way, what trade-offs the committee made, where the sharp edges are in real production code, and how to answer the interview questions that trip up candidates who only skimmed the release notes. We'll write real, runnable code for each feature and look hard at the moments where things go wrong.
Why C++20 Coroutines Are Not Just Syntactic Sugar
C++20 coroutines are stackless, resumable functions that suspend execution at a suspension point (co_await, co_yield, co_return) without blocking the thread. Unlike stackful coroutines (e.g., Boost.Coroutine2), each coroutine frame is heap-allocated and contains only the live variables across suspension points — no full stack copy. This makes them lightweight: a suspended coroutine costs roughly 100–200 bytes, not megabytes.
The compiler transforms the function into a state machine. Each suspension point becomes a label; the coroutine frame stores the program counter and spilled registers. Resumption jumps back to the correct label. This zero-overhead abstraction means you pay only for what you suspend — no hidden allocation if you never suspend. But the frame lifetime is decoupled from the caller: if you destroy the coroutine handle without resuming, the frame leaks.
Use coroutines when you need to write asynchronous code that reads like synchronous logic — network I/O, generator pipelines, or cooperative multitasking. They are not a replacement for threads; they are a tool to express non-blocking waits without callback hell. In production, the most common mistake is assuming the coroutine frame lives as long as the function scope — it doesn't. The frame persists until the coroutine completes or the handle is destroyed.
⚠ Frame Lifetime Trap
A coroutine frame outlives its calling scope. If you capture a reference to a local variable and the caller returns, the reference dangles.
📊 Production Insight
Teams migrating from Boost.Coroutine2 to C++20 coroutines often hit crashes after ~200 frames because the default heap allocator fragments under rapid allocation/deallocation of coroutine frames.
The symptom: a segfault in the coroutine frame destructor, not at the suspension point, making it look like a logic error.
Rule of thumb: always provide a custom allocator (via promise_type::operator new) or use a pool allocator when coroutine frames are created at high frequency.
🎯 Key Takeaway
Coroutine frames are heap-allocated and manually lifetime-managed — treat them like unique_ptr, not stack variables.
co_yield crashes after many frames are usually allocator fragmentation, not coroutine logic bugs.
Always pair coroutine creation with RAII handle wrappers to guarantee frame cleanup on early exit.
thecodeforge.io
Cpp20 Features
1. Concepts: Contracts for Template Parameters
Concepts allow you to specify constraints on template arguments that are checked at compile time, replacing SFINAE with readable, maintainable predicates. A concept is a compile-time boolean expression evaluated on the type — if it fails, the template is excluded from overload resolution (not an error unless no valid overload exists).
Before C++20, you'd use std::enable_if or tag dispatch to achieve the same. The difference? Concepts produce error messages that actually tell you what's wrong — not a 200-line template instantiation backtrace.
The standard library already provides predefined concepts like std::integral, std::floating_point, std::default_initializable, and std::ranges::range. You compose them with logical operators (&&, ||) and the requires clause.
concepts_example.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <concepts>
#include <iostream>
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
template<Addable T>
T add(T a, T b) {
return a + b;
}
intmain() {
std::cout << add(3, 4) << '\n'; // OK: int satisfies Addable// std::cout << add(std::string("Hi"), std::string("!")); // Error: std::string does not satisfy Addablereturn0;
}
Output
7
🔥Key Insight
Concepts are not runtime checks. They are purely compile-time constraints that participate in overload resolution. A requires expression can also assert on the existence of member functions and nested types — not just operators.
📊 Production Insight
Concepts fail silently when a constraint is too loose — e.g., accepting any type with + but expecting a numeric type. Later operations like sqrt will produce deep template errors.
Always pair concepts with static_assert in the implementation to catch logic errors early.
Rule: Write concepts that are strict enough to reject misuse but not so strict that they reject valid types with the same interface.
🎯 Key Takeaway
Concepts replace SFINAE with readable contracts.
They produce errors at the call site, not inside the template.
Write concepts that match the semantic intent, not just syntactic capability.
When to Use Concepts vs SFINAE
IfNeed to constrain a single template parameter with a simple type trait
→
UseUse a concept from <concepts> or define your own with requires. Avoid SFINAE entirely.
IfNeed to conditionally enable a function based on multiple type properties
→
UseUse a concept with logical operators (&&, ||). Still cleaner than enable_if.
IfYou must support C++17 or earlier
→
UseStick with SFINAE (enable_if, void_t). You can't use concepts in C++17.
2. Ranges: Composable Lazy Data Pipelines
Ranges bring the composability of functional programming to C++ containers and views. A range is anything that can be iterated over — arrays, vectors, strings, or custom types. Views (std::views) are composable adaptors that lazily transform or filter ranges without allocating new containers.
Instead of writing nested loops or manually chaining algorithms, you write a pipeline: vec | views::filter(pred) | views::transform(f) | views::take(n). Each stage is lazy — only the elements actually needed are processed.
The performance win is twofold: no memory allocation for intermediate results, and early termination when using take or drop_while. But beware of dangling references: views store iterators, and if the underlying range dies, the view becomes invalid.
ranges_pipeline.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <ranges>
#include <vector>
#include <iostream>
intmain() {
std::vector<int> data = {1, 2, 3, 4, 5, 6};
auto result = data
| std::views::filter([](int x){ return x % 2 == 0; }) // lazy: none executed yet
| std::views::transform([](int x){ return x * x; })
| std::views::take(3); // only first 3for (int v : result) {
std::cout << v << ' ';
}
// Output: 4 16 36return0;
}
Output
4 16 36
⚠ Dangling View Danger
Views store iterators to the underlying range. If that range is a temporary (prvalue), the view's iterators dangle — accessing them is undefined behaviour. Always ensure the underlying range outlives all views.
📊 Production Insight
Lazy evaluation means errors (like an invalid iterator) are deferred until iteration. A pipeline that compiles fine may crash at runtime if a view depends on a temporary.
Use std::views::common to convert a view into a range that can be used with older APIs expecting begin/end pair.
Tip: In performance-critical loops, avoid views::filter before views::transform if the filter rejects most elements — the transform spens work on filtered-out elements anyway? Actually, filter rejects before transform, so transform only runs on passing elements. That's correct.
🎯 Key Takeaway
Ranges compose lazy pipelines without extra allocations.
Watch out for dangling views — keep underlying data alive.
Use take before expensive transforms to limit work.
When to Use Ranges vs Traditional Algorithms
IfNeed multiple transformations on a sequence
→
UseUse ranges pipeline — cleaner and lazy allocation.
IfWorking with very large data and need early termination
→
UseRanges with take or drop_while avoid processing the entire sequence.
IfYou need to pass the result to a legacy function expecting vector
→
UseMaterialise the range into a container: auto vec = range | ranges::to<std::vector>(); (C++23) or manually copy.
thecodeforge.io
Cpp20 Features
3. Coroutines: Cooperative Multitasking Without Threads
Coroutines are functions that can suspend execution and resume later, preserving local state across suspension points. Unlike threads, coroutines don't require OS scheduling — the decision to suspend and resume is cooperative.
C++20 provides three keywords:co_await (suspend and wait for an awaitable), co_yield (produce a value and suspend), and co_return (final return from a coroutine). The compiler transforms the function into a state machine, allocating a coroutine frame on the heap (by default) to hold suspended state.
Coroutines shine in scenarios like streaming data, asynchronous I/O, and generators. But they have subtle pitfalls: heap allocation overhead, potential stack overflow if deeply nested, and difficult debugging because the stack is reconstructed at suspension points.
By default, the coroutine frame is dynamically allocated. Use promise_type::get_return_object_on_allocation_failure to avoid allocation failure. Many production coroutine libraries (e.g., boost::asio) provide custom allocators to reduce overhead.
📊 Production Insight
Coroutine frames allocate on the heap by default — make sure the allocation path is fast. Use std::experimental::generator or a custom promise with a pool allocator for high-throughput scenarios.
Debugging suspended coroutines is hard: a debugger may show a truncated stack. Use coroutine_handle::resume explicitly in unit tests to control scheduling.
Rule: Never use coroutines for short-running synchronous tasks — the allocation overhead (typically 50-100 bytes) outweighs the benefit.
🎯 Key Takeaway
Coroutines suspend without blocking threads.
Heap allocation per coroutine frame is the default — optimise with custom allocators for hot paths.
Debugging suspended coroutines requires specialised tools; test with explicit resumption.
Use Coroutines When...
IfYou need a lazy sequence that produces values on demand
→
UseUse co_yield generator pattern.
IfYou have asynchronous I/O bound operations
→
UseUse co_await with a proper executor (boost::asio, libunifex).
IfThe function is called rarely or latency-insensitive
→
UseCoroutines add overhead; use plain functions or threads.
4. Modules: Modern Compilation Boundaries
Modules (export, import) provide a new way to organise C++ code that is faster to compile and more hygienic than headers. A module interface unit (.cppm) declares what is exported, and module implementation units (.cpp) define non-exported details. Importing a module gives access only to the exported names, eliminating macro leaks, ODR violations, and include-order dependencies.
Modules also improve build times because the compiler pre-compiles module interfaces into .pcm files (Clang) or .ifc files (MSVC). Translation units that import the module only need to load that precompiled interface — no reparsing of headers.
However, module support is not fully consistent across compilers. You may encounter issues with .cppm vs .ixx extensions, and interaction with precompiled headers (PCH) can be fragile.
module_math.cppmCPP
1
2
3
4
5
6
7
8
9
10
11
12
export module math;
exportintadd(int a, int b) {
return a + b;
}
exportintmultiply(int a, int b) {
return a * b;
}
// Non-exported: not visible to importersinthelper(int x) {
return x * 2;
}
⚠ Compiler Module Support Differences
GCC uses .cppm and -fmodules-ts. Clang uses .cppm and -fmodules. MSVC uses .ixx for module interface and /interface. The extension matters — using .h for a module interface won't work. Always check your compiler's documentation for exact file extension and flags.
📊 Production Insight
Modules eliminate the preprocessor's textual inclusion, but they introduce a new dependency on compiler-specific binary formats. A module compiled by GCC cannot be imported by Clang.
In mixed-compiler environments (common in large organisations), stick to headers or use a build system that recompiles modules per compiler.
Tip: Start with a single module to isolate commonly used utilities. Gradually expand, but monitor build cache invalidation — module dependencies are fine-grained and can cause rebuild cascades.
🎯 Key Takeaway
Modules replace headers with proper compilation boundaries.
Compiler support is uneven — test your build matrix.
Start small: convert the most-frequently-included headers first.
Modules vs Headers Decision
IfProject is header-only and you want faster builds
→
UseConvert key interfaces to modules. Expect a 20-30% build time reduction for large projects.
IfYou need to distribute a closed-source library
→
UseModules can expose only the API, hiding implementation details, but binary module format compatibility is poor across compilers. Headers are still safer.
IfProject uses many macros or #pragma once workarounds
→
UseModules solve include guard issues and macro pollution. Strong incentive to migrate.
5. Three-Way Comparison Operator (Spaceship)
The <=> operator (also called the spaceship operator) performs a three-way comparison, returning a comparison category type that indicates less, equal, or greater. The compiler can automatically generate ==, !=, <, <=, >, >= from <=> if you write = default. This eliminates the boilerplate of writing six comparison operators for a class.
There are three category types:std::strong_ordering (total order: no two values are incomparable), std::weak_ordering (equivalence classes, e.g., case-insensitive strings), and std::partial_ordering (values may be incomparable, e.g., floating-point NaN).
The operator is a two-way operator with reversed candidate generation, which can lead to ambiguities when both <=> and == are custom defined.
When you = default<=>, the compiler generates == and != only if <=>'s return type is one of std::strong_ordering or std::weak_ordering (i.e., not partial_ordering). For partial_ordering, you must also define == explicitly if you need equality.
📊 Production Insight
Ambiguities arise when a class has both <=> and a custom operator==. The compiler sees reversed candidates for <=> and may refuse to compile. The fix: remove the custom operator== if it does the same as the generated one, or qualify calls with explicit operator<=>.
For floating-point members, <=> returns partial_ordering — be aware that NaN comparisons will be unordered, which affects sorting containers like std::set. Use std::strong_ordering only if you guarantee no NaN.
🎯 Key Takeaway
<=> with = default generates all six relational operators.
Watch for ambiguity when manually defining both <=> and ==.
Floating-point members produce partial_ordering — handle NaN explicitly.
When to Default Spaceship
IfClass has simple value semantics (all members comparable)
→
UseUse auto operator<=>(const T&) const = default;. Generates all six operators cleanly.
IfClass contains floating-point members that may be NaN
→
UseDefine operator<=> manually to return strong_ordering after handling NaN, or accept partial_ordering but be aware of std::set incompatibility.
IfYou need only equality/hash, not ordering
→
UseJust define == and a hash function. Don't add <=> unless you need sortability.
6. std::to_array: Stop Using Raw Arrays in Templates
We had a bug last month. A junior wrote a template function expecting an array of integers, passed a braced-init-list, and got template deduction failure. The compiler couldn't figure out what type {1,2,3} was. Enter std::to_array. It's a factory function that deduces both size and element type from an initializer list at compile time. No more std::array size template arguments that mismatch. No more silent decays to pointers. The WHY: braced-init-lists have no type in template context. std::to_array converts them to a proper std::array with a concrete type. Use it for any compile-time sized container you pass into template code. It's in <array> and costs nothing at runtime. The compiler will reject mismatched sizes before your CI pipeline runs.
Don't use std::to_array on std::vector or dynamic data. It copies the elements at compile time, not runtime. Use std::array::data() and manual copy if you need dynamic data into a fixed-size array.
🎯 Key Takeaway
Use std::to_array whenever you need a compile-time array from an initializer list inside templates. It fixes deduction failures.
7. [[likely]] and [[unlikely]]: Assert Your Branch Expectations to the Optimizer
Your hot path was slow. We profiled. The branch predictor was thrashing on a 50/50 split that should have been 95/5. The compiler didn't know. [[likely]] and [[unlikely]] are attributes that annotate which branch the CPU should predict. No functional change. Pure performance hint. WHY: branch misprediction stalls cost 10-20 cycles per miss. In tight loops, that adds up. Place [[likely]] on the path that runs 95% of the time—error checks, fast-return paths, early exits. Place [[unlikely]] on error handling. The compiler will reorder blocks to minimize taken branches. It works in if/else, switch cases, and loop conditions. One caveat: don't lie. If you mark a branch [[likely]] that actually runs 50%, performance degrades. Profile first, annotate second.
likely_unlikely.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
// io.thecodeforge
#include <iostream>
#include <random>
#include <chrono>
intparse_input(int value) {
// Expected: most values are > 0if (value > 0) [[likely]] {
return value * 2;
} else [[unlikely]] {
// Error path: log and return -1
std::cerr << "Invalid input: " << value << "\n";
return -1;
}
}
intmain() {
std::mt19937 gen(42);
std::uniform_int_distribution<int> dist(-10, 100);
longlong sum = 0;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10'000'000; ++i) {
sum += parse_input(dist(gen));
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Sum: " << sum << "\n";
std::cout << "Time: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< "ms\n";
return0;
}
Output
Invalid input: -3
Invalid input: -8
...
Sum: 1000435000
Time: 124ms
🔥Compiler Support:
GCC 9+, Clang 9+, and MSVC 19.25+ support these attributes. Check your compiler version before deploying. Older compilers ignore them silently.
🎯 Key Takeaway
Use [[likely]] on fast paths and [[unlikely]] on error handling. Never guess—profile your branches first.
8. Map and Set .contains(): Kill the .count() Hack for Good
I've seen codebases with if (my_map.count(key) > 0) scattered everywhere. That works, but it's wasteful. std::map::count has to traverse the tree to count all matches—which returns 0 or 1 for unique keys. Inefficient and confusing. C++20 gives us .contains(key) which returns bool directly. It communicates intent: "does this key exist?" not "how many times does this key appear?". The WHY: readability and performance. contains() can short-circuit after finding the first match. For unordered containers, it's a single hash lookup. For ordered maps, it's one tree traversal, not a full count. Replace all your .count() > 0 checks. It's a safe mechanical transformation. Your code will be clearer. Your code reviewers will thank you. Your profiler will show lower CPU usage.
Don't use .contains() if you need to access the element after checking existence. Use .find() and compare against .end() instead. contains() doesn't return an iterator.
🎯 Key Takeaway
Replace all map.count(key) > 0 with map.contains(key). It's faster and clearly expresses presence checking.
● Production incidentPOST-MORTEMseverity: high
Coroutine Stack Exhaustion in a Production Video Transcoding Pipeline
Symptom
Process crashes with SIGSEGV after processing ~200 frames. Stack traces show deep call chains inside an std::generator function. Memory usage grows linearly with frame count until the stack overflows.
Assumption
Engineers assumed coroutines would automatically yield control at each co_yield, preventing deep recursion. The generator was actually calling itself recursively without a base case.
Root cause
The generator coroutine used recursion to produce frames but never returned from the recursive call — it kept suspending at co_yield but never allowed the promise to destroy the frame. The coroutine frame remained on the heap and the call stack grew unboundedly.
Fix
Replace the recursive generator with an iterative loop that yields each frame. Alternatively, use a flat recursion depth check and fallback to thread spawning when depth exceeds a threshold.
Key lesson
Coroutines don't automatically protect against infinite recursion — they just shift the allocation to the heap, but the call stack still grows for each nested suspend.
Always test coroutines with stress inputs that can cause deep nesting. Stack usage analysis is vital before production deployment.
Prefer iterative state machines over recursive generators for unbounded data flows.
Production debug guideSymptom-to-action guide for common production issues4 entries
Symptom · 01
Template instantiation error: 'constraints not satisfied' with deep concept chain
→
Fix
Use -fconcepts-diagnostics-depth=2 (GCC) or -Xclang -fconcepts-ts (Clang) to print only the first failing constraint. Avoid parsing the full substitute failure tree.
Symptom · 02
Coroutine heap frame is not freed — memory usage grows with each suspension
→
Fix
Check if the coroutine's return type uses std::noop_coroutine or a custom promise with final_suspend. Ensure the coroutine handle is destroyed or the promise's return_void properly destroys the frame.
Symptom · 03
Module import results in 'not a module' error even though .cppm file exists
→
Fix
Verify that the module file is compiled with -std=c++20 -fmodules-ts (GCC) or -std=c++20 -fmodules (Clang). Check that the module interface unit has the .cppm extension and includes export module mymodule;.
Symptom · 04
Spaceship operator <=> generates wrong comparison order for custom types
→
Fix
If you defined operator<=> manually, ensure it returns a comparison category type (std::strong_ordering, etc.). For generated ==/!=, the compiler synthesises from <=> only if operator== is not explicitly defined.
★ C++20 Debug Cheat SheetQuick commands and checks for common C++20 production issues
Concept constraint not satisfied; error message is huge−
Coroutines suspend without threads but heap-allocate frames by default
optimise for hot paths.
4
Modules improve build times and eliminate include order issues, but compiler support is uneven.
5
Spaceship operator auto-generates comparisons but watch for floating-point partial ordering and ambiguity with custom ==.
6
Adopt C++20 features incrementally
start with concepts and spaceship, then ranges, then modules and coroutines as your compiler maturity allows.
Common mistakes to avoid
4 patterns
×
Using `requires` without understanding substitution failure
Symptom
A concept matches a type that passes the requires expression but fails on a later constraint. Error messages point to the instantiation site, not the concept, hiding the real cause.
Fix
Test each sub-expression in the requires clause separately. Use static_assert(MyConcept<T>) at the template definition to verify constraints are sufficient.
×
Assuming coroutine frame is stack-allocated or cheap
Symptom
High latency in tight loops that call a coroutine. Profiling shows allocation pressure and memory fragmentation.
Fix
Benchmark the coroutine overhead. For hot paths, consider using a custom promise with a memory pool, or refactor to a plain function with manual state machine.
×
Mixing modules with `#include` in the same translation unit
Symptom
Build errors like 'module declaration must appear before any preprocessor directives'. The compiler rejects the file because #include before export module is not allowed (except for headers used by the module itself).
Fix
Move all #include statements after the export module line, or use import for dependencies. Only import <iostream> style is allowed, not #include.
×
Defining both `<=>` and `==` in a class that should be value-ordered
Symptom
Compiler error about ambiguous overload when comparing two objects — the reversed candidate from <=> conflicts with the custom ==.
Fix
Remove the custom operator== if it does the same as the generated one. If you need different equality semantics, define <=> manually and not use = default.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Explain how concepts differ from SFINAE and why they improve error messa...
Q02SENIOR
When would you use ranges instead of traditional `std::transform` and `s...
Q03SENIOR
Explain the coroutine frame allocation and how to optimise it.
Q01 of 03SENIOR
Explain how concepts differ from SFINAE and why they improve error messages.
ANSWER
SFINAE (Substitution Failure Is Not An Error) uses tools like std::enable_if to conditionally disable templates. When a substitution fails, the compiler silently removes that overload and tries others — resulting in opaque error messages if no viable overload exists. Concepts replace this with explicit compile-time predicates. If a concept is not satisfied, the compiler reports exactly which constraint failed and on what type, at the call site. Concepts also participate in overload resolution as ordered constraints (using constraint subsumption), allowing partial ordering without additional traits.
Example:
``cpp
template<typename T>
requires std::integral<T>
T add(T a, T b) { return a + b; }
`
If called with float`, error says: 'constraints not satisfied: std::integral<float>' — far clearer than SFINAE noise.
Q02 of 03SENIOR
When would you use ranges instead of traditional `std::transform` and `std::copy`? What about performance?
ANSWER
Ranges are best when you need a pipeline of operations (filter → transform → take) without intermediate storage. Traditional algorithms require allocating vectors at each stage. For example, extracting the first 10 even numbers squared from a large vector: with ranges it's data | filter(even) | transform(square) | take(10) — lazy, no allocation. With traditional algorithms you'd need std::copy_if into a new vector, then std::transform into another, then resize. Performance: ranges avoid allocations but add small overhead per element due to iterator adaptors. For small lists (<1000 elements) the difference is negligible; for large lists ranges win on memory. Additional overhead: views are not tracked by address sanitizer — memory errors in views may manifest as use-after-free.
Q03 of 03SENIOR
Explain the coroutine frame allocation and how to optimise it.
ANSWER
When a function becomes a coroutine (uses co_await, co_yield, or co_return), the compiler allocates a coroutine frame on the heap (by default) to store the function's local state across suspension points. This frame persists until the coroutine completes. By default, it uses operator new via the promise's get_return_object.
Optimisation strategies:
1. Move the allocation to the return object — some libraries use get_return_object_on_allocation_failure to avoid exception handling.
2. Provide a custom promise with a memory pool: override operator new and operator delete in the promise type to allocate from a pre-allocated arena.
3. In rare cases, use std::experimental::generator with noop_coroutine (no dynamic allocation), but only for trivial generators.
4. Force in-place allocation with use allocator_arg pattern: pass an allocator to the coroutine, which forwards to promise. This is supported via std::coroutine_traits.
Always benchmark: the default allocation is typically fine for infrequent async calls but kills latency in tight loops.
01
Explain how concepts differ from SFINAE and why they improve error messages.
SENIOR
02
When would you use ranges instead of traditional `std::transform` and `std::copy`? What about performance?
SENIOR
03
Explain the coroutine frame allocation and how to optimise it.
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
What C++20 features are production-ready in 2026?
Concepts, Ranges, and the spaceship operator are well supported in GCC 11+, Clang 14+, and MSVC 19.28+. Modules are production-ready with Clang and MSVC; GCC support is still experimental (requires -fmodules-ts). Coroutines are solid in all three, but the standard library support (e.g., std::generator) is not in C++20 — you need C++23 or a third-party library. For greenfield projects, you can safely use Concepts, Ranges, and Spaceship today.
Was this helpful?
02
How do I convert a legacy header to a module?
Create a .cppm file (or .ixx on MSVC) with export module mymodule; at the top. Move the declarations you want to export (functions, classes, etc.) and prefix each with export. Remove #pragma once and include guards. Include any required headers inside the module (after the module declaration) using #include — but prefer import for other modules. Then compile the module interface to produce the binary module file, and import it from other translation units. Expect some adjustments for inline functions and template definitions.
Was this helpful?
03
Can I use coroutines without heap allocation?
Yes, but it's not trivial. If your coroutine never suspends (always completes inline), the compiler may elide the heap allocation — check with -std=c++20 -fcoroutines-ts -O2 and verify no calls to operator new. For coroutines that do suspend, you can override promise_type::get_return_object_on_allocation_failure to return nullptr and handle allocation failure. Alternatively, use a custom allocator within the promise type (via std::coroutine_traits). Some libraries like cppcoro provide a custom_allocator coroutine. But the safest approach for production is to accept the allocation and optimise with pool allocators.
Was this helpful?
04
Why does the spaceship operator sometimes cause ambiguous comparisons?
The three-way comparison operator <=> is a two-way operator: when comparing a <=> b, the compiler also considers b <=> a (reversed candidate). If you define both <=> and a custom operator==, the reversed <=> can conflict with the custom == when the compiler tries to use operator== from the reversed candidate. The fix: either remove the custom operator== if it matches the generated one, or don't use = default for <=> — write it manually and avoid the ambiguity. In C++20, the rule is: don't define both <=> and == unless you need different semantics.