Variadic Templates, if constexpr & C++20 Concepts
In modern C++, we never guess what the compiler does. We look directly at byte offsets, CPU registers, and object ownership lifetimes.
A regular if statement evaluates its condition at runtime and requires both the true and false blocks to be fully valid C++ for all instantiated template types. if constexpr evaluates its condition at compile time: the compiler discards the unselected branch, completely skipping template instantiation for that block. This prevents compilation errors when calling member functions or operators that only exist on specific types.
Pre-C++20 SFINAE required verbose std::enable_if_t expressions in template parameter lists, return types, or default arguments. When substitution failed or was ambiguous, compilers output cryptic, unreadable errors spanning multiple pages. C++20 Concepts integrate directly into function signatures, support intuitive requires expressions, enable concept subsumption for overload resolution, and produce clear error messages showing exactly which requirement failed.
Subsumption is the compiler's ability to determine if concept A is more specialized than concept B. The compiler normalizes both concepts into disjunctive/conjunctive normal forms of atomic constraints. If concept A logically implies concept B (A implies B), concept A subsumes B and is considered more constrained. When an argument matches both overloads, the compiler chooses the more constrained overload with zero ambiguity.
Interviewer's lens
Variadic templates, if constexpr, and C++20 concepts represent the greatest leap in C++ generic programming in twenty years. Weak candidates still write cumbersome pre-C++17 recursive template overloads or unreadable std::enable_if SFINAE hacks that yield screen-filling compiler errors. Strong candidates leverage fold expressions to eliminate recursive base-case templates, use if constexpr to prune uninstantiated AST branches at compile time, and apply C++20 concepts with subsumption ordering to make generic code as readable and strictly typed as concrete classes.
Under the hood & memory layout
When a compiler encounters a variadic template template <typename... Args>, it represents Args as a type pack in the Abstract Syntax Tree (AST). Prior to C++17, unpacking required recursive template instantiations that spawned dozens of intermediate template symbol instances, bloating compile times and object files. C++17 fold expressions lower directly into binary expressions within the active AST node (e.g. (args + ...) lowers to (a1 + (a2 + (a3 + a4)))), generating zero function calls and zero recursion. When evaluating if constexpr, the compiler only instantiates the active branch for the deduced type T: code inside discarded branches is checked for basic grammatical syntax but is completely excluded from template instantiation, allowing operations (like val.begin()) that would be illegal on non-container types. In C++20, Concepts model compile-time predicates evaluated during overload resolution: if overload A's constraints subsume overload B's constraints, A is strictly more constrained and selected without ambiguity.
Args... holds a sequence of 0 or more types; sizeof...(Args) evaluates to an integer literal constant at compile time with zero runtime storage.
Fold expressions like (args && ...) expand inline into chained operators in machine registers, short-circuiting at the first false without recursive stack frames.
Inside if constexpr, discarded blocks are not compiled into machine code. Type errors inside discarded blocks (e.g. calling .size() on an int) are ignored because the template is never instantiated for that branch.
The compiler forms a logical conjunction graph of atomic constraints. If concept X requires concept Y plus additional conditions, X subsumes Y and wins overload resolution without ambiguous match errors.
Core concepts & syntax traps
Variadic Templates & Parameter Packs: Packing and Unpacking
Variadic Templates & Parameter Packs: Packing and Unpacking
A template parameter pack typename... Args accepts zero or more template arguments. Inside the function or class, args... is a function parameter pack. You can query the number of elements at compile time with sizeof...(Args) or sizeof...(args).
Prior to C++17, extracting values required writing a recursive helper function paired with a non-variadic base-case overload. Modern C++ eliminates this boilerplate entirely using Fold Expressions or if constexpr.
template <typename... Args>
void countItems(Args... args) {
constexpr std::size_t count = sizeof...(Args); // Evaluated at compile time
std::println("Received {} arguments", count);
}
countItems(1, "test", 3.14); // Prints: Received 3 argumentssizeof...(Args) counts pack elements at compile time with zero runtime overhead.
C++17 Fold Expressions: 4 Varieties & The Comma Operator Trick
C++17 Fold Expressions: 4 Varieties & The Comma Operator Trick
C++17 fold expressions expand a parameter pack over a binary operator directly without recursion. There are 4 distinct forms:
| Fold Type | Syntax | Expansion |
|---|---|---|
| Unary Right Fold | (args op ...) | (arg1 op (arg2 op ... op argN)) |
| Unary Left Fold | (... op args) | (((arg1 op arg2) op ...) op argN) |
| Binary Right Fold | (args op ... op init) | (arg1 op (... op (argN op init))) |
| Binary Left Fold | (init op ... op args) | (((init op arg1) op arg2) op ... op argN) |
Using the comma operator fold ((statement(args)), ...), you can execute any statement or function once for every element in the pack in strict left-to-right order.
template <typename... Args>
auto sum(Args... args) { return (args + ...); } // Unary right fold
template <typename... Args>
void printEach(const Args&... args) {
// Comma fold: executes print for each argument sequentially
((std::print("{} ", args)), ...);
std::println("");
}Enclose fold expressions in parentheses: (args + ...) or ((doWork(args)), ...) to perform loop-free pack expansion.
Compile-Time Branching with if constexpr: Discarded Branch Pruning
Compile-Time Branching with if constexpr: Discarded Branch Pruning
A traditional if (condition) statement evaluates at runtime, meaning both the true and false branches must be fully valid C++ for every type instantiated! If one branch attempts to call .size() on an int, the entire template fails to compile, even if that branch is unreachable.
With if constexpr (condition), the condition is evaluated at compile time. The unselected branch is discarded: the compiler skips template instantiation for the discarded body. This allows conditional code paths that would otherwise produce fatal compilation errors on incompatible types.
template <typename T>
auto getLength(const T& val) {
if constexpr (requires { val.size(); }) {
return val.size(); // Only instantiated if val.size() is valid!
} else {
return 1; // Instantiated for ints, floats, etc.
}
}Use if constexpr to write clean, type-dependent code without instantiating invalid operations on mismatched types.
C++20 Concepts & requires Clauses: Replacing Cryptic SFINAE
C++20 Concepts & requires Clauses: Replacing Cryptic SFINAE
Before C++20, constraining templates required std::enable_if_t, which obscured function signatures and generated hundreds of lines of impenetrable compiler errors on failure. C++20 Concepts turn type requirements into first-class language constructs:
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template <Numeric T>
T add(T a, T b) { return a + b; }
If a caller passes std::string, the compiler produces a crisp diagnostic: 'constraints not satisfied: std::string does not satisfy Numeric'.
template <typename T>
concept Hashable = requires(T a) {
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
template <Hashable T>
void store(const T& item) { /* guaranteed hashable */ }Replace std::enable_if with C++20 concepts and requires clauses for readable APIs and intuitive compiler errors.
Concept Subsumption & Overload Resolution: The 'More Constrained' Rule
Concept Subsumption & Overload Resolution: The 'More Constrained' Rule
When multiple function templates match a call, the compiler checks if one concept subsumes another. If concept A requires concept B and adds additional atomic constraints (i.e. A implies B), concept A is more constrained than B.
The compiler selects the more constrained overload without reporting an ambiguity error! This enables clean hierarchical dispatch: e.g. selecting an O(1) random-access algorithm over an O(N) bidirectional algorithm.
template <std::bidirectional_iterator It>
void step(It& it) { ++it; } // Overload 1: Bidirectional
template <std::random_access_iterator It>
void step(It& it) { it += 1; } // Overload 2: Random Access (Subsumes Bidirectional!)
// Calling step(vector.begin()) calls Overload 2 cleanly without ambiguity!The compiler automatically picks the most constrained concept overload via subsumption ordering.
Unlock Live Implementation & Interview Scenarios
The C++23 production code, live coding steps, scenario twist solutions, and common UB traps are part of Brute2Best Pro. Module 1 is completely free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.