C++ Template Interview Questions: Deduction, SFINAE, and Concepts
Quick Overview
Trace C++ template deduction, declaration substitution, constraints and selected-body errors with compiled positive and negative examples. The worked overload matrix separates syntactic acceptance from semantic contracts.
A C++ template question often turns on one small change: move v.size() from a trailing return type into the function body, and a fallback stops working. Memorizing “SFINAE means substitution failure is not an error” will not explain that result. You need to identify where deduction ends, which candidates remain, and when a body must be instantiated.
This guide uses original, reproducible exercises to practice those boundaries. Start with Implement and Defend a Generic C++ Stack, then use the traces below to explain which types your interface accepts and why. The goal is to explain your API contract, including the calls it rejects.
The language rules below come from the public C++ working-draft mirror; compiler results are observations from Apple clang 21.0.0. These are teaching exercises, not reports of questions asked by an employer. The examples use C++17 or C++20 as marked; newer draft wording should not be treated as a promise about every older compiler.

How should you trace a template call?
Start with four checkpoints: deduce template arguments, check substitution and constraints, select the best viable overload, then instantiate the body when needed. This is a diagnostic map, not a complete specification of every compiler operation. For example, an auto return type can require body instantiation while a call is being resolved.
For each checkpoint, name the evidence you would expect: conflicting deductions, a discarded candidate, ambiguity, or an error inside an instantiated specialization. The final “no matching function” line describes the outcome; the candidate notes explain why it happened. Read the candidate notes and the instantiation chain too.
Predict the result before changing code. Say which parameter determines T, whether a conversion is available at that stage, and what must become well formed for the call to work. Then propose the smallest change that matches the intended interface. An unexplained cast can make the call compile while leaving the contract unclear.
Why does deduction reject an int and a double?
Consider this C++17 function:
template<class T>
T larger(T a, T b) { return a < b ? b : a; }
larger(3, 4); // T = int; returns 4
larger(3, 4.5); // deduction fails
larger<double>(3, 4.5); // T fixed explicitly; returns 4.5
The official function-call deduction rules compare parameter types with argument types. In this example, the first argument deduces int, while the second deduces double for the same T. Deduction does not choose a convenient common arithmetic type to reconcile them. Our compiler rejected the middle call with a note identifying the conflicting types.
Explicitly specifying double changes the problem. There is no conflicting deduction for T; the arguments can initialize the now-known parameter types. That explains this repair, but it does not establish that conversion is appropriate. An interface comparing monetary amounts, large integers, or user-defined quantities may need different conversion rules.
An alternative is two template parameters and an explicitly designed result policy. Before writing that version, answer what happens to signed and unsigned values, what result type callers receive, and whether comparison and conversion preserve the meaning of the inputs. “Use auto everywhere” leaves those decisions unresolved.
What happens to const and references?
The same deduction rules also distinguish by-value and reference parameters. With an ordinary const int lvalue, a by-value T parameter deduces int; a T& parameter deduces const int. For a forwarding reference T&&, an int lvalue makes T deduce as int&, and reference collapsing yields an int& parameter.
| Declaration and argument | Deduced T | Consequence to explain |
|---|---|---|
f(T), const int lvalue | int | Top-level const does not become part of this by-value T. |
f(T&), const int lvalue | const int | The reference preserves the referred-to const qualification. |
f(T&&), int lvalue, with deduced T | int& | This is forwarding-reference deduction followed by reference collapsing. |
These three cases passed local static_assert checks. Do not generalize the last row to every spelling of T&&: the forwarding-reference rule has specific conditions. Also separate deduction from ownership. A successfully deduced reference says nothing by itself about whether returning or storing that reference is safe.
When does SFINAE choose the fallback?
Here is a complete C++17 overload pattern, assuming the usual vector header:
template<class T>
auto extent(const T& v) -> decltype(v.size()) {
return v.size();
}
int extent(...) { return -1; }
For a vector with three elements, the template returns its size. For extent(42), substituting int into the trailing return type makes v.size() invalid. That failure occurs in the relevant immediate context, so the template does not provide a viable candidate for this call. The ellipsis fallback remains. Our compiled program printed 3, then -1.
The official deduction and substitution rules limit this protection to specified immediate contexts. They also warn that substitution can trigger other instantiations whose errors are outside that protection. “It happened while processing a template” is therefore insufficient evidence for SFINAE.
Now change only the return-type arrangement:
template<class T>
auto extent(const T& v) {
return v.size();
}
int extent(...) { return -1; }
// extent(42); // hard error when the body is instantiated
In our negative compile test, the diagnostic identified the body of extent<int>. The compiler did not discard this overload and retry the ellipsis fallback. Deducing the return type requires instantiating the body; that body is not the immediate context protected by SFINAE.
This makes a useful interview counterexample: both versions contain the same invalid member access, yet its location changes the result. A repair should expose the acceptance condition in the declaration, or deliberately implement valid behavior inside the body. A fallback returning -1 is only a teaching device here; a production API should define an unambiguous result and error policy rather than mixing size and sentinel conventions casually.
What do concepts change in C++20?
A concept lets you name a requirement and constrain an overload using it:
#include <concepts>
#include <cstddef>
template<class T>
concept Sized = requires(const T& v) {
{ v.size() } -> std::convertible_to<std::size_t>;
};
template<Sized T>
std::size_t extent(const T& v) { return v.size(); }
int extent(...) { return -1; }
The requires-expression rules explain the expression inside Sized: it checks the stated requirements without executing v.size(). The compound requirement checks that the expression is valid and that its result satisfies the specified conversion constraint. The constrained template declaration then uses that concept as an acceptance condition, following the rules for associated constraints.
Keep the two roles distinct. A requires-expression produces a Boolean result describing whether requirements are satisfied. A requires-clause attaches a constraint to a declaration. The abbreviated template<Sized T> syntax above is another way to constrain the template parameter; it is not a runtime guard.
Our C++20 executable again printed 3 for the vector and -1 for the integer. Those outputs verify the fallback behavior for these two inputs. They do not prove concepts catch every possible error: an accepted type can still cause an invalid operation in the body if the declaration omits a necessary requirement.
Nor does convertible_to<std::size_t> prove a useful size contract. It does not establish constant-time complexity or guarantee that a custom implementation reports the number of accessible elements. State those semantic expectations separately and test them where possible. Syntax checks are valuable precisely when you can explain their boundary.
Can a constrained function still return a bad size?
Try a type whose member exists but reports a negative count:
struct SignedCount {
int size() const { return -1; }
};
static_assert(Sized<SignedCount>);
// extent(SignedCount{}) converts -1 to std::size_t.
The concept accepts this type in our C++20 check. Its member can be called on a const object, and its integer result can be converted to the required unsigned type. That is the stated requirement. It is a different question whether the returned number represents a valid container size. The resulting unsigned value is not a useful count for this object; successful overload selection has not repaired its meaning.
The working draft's convertible_to definition specifies the conversion requirement. It does not add a nonnegative-count rule to an arbitrary member named size. The requires-expression also does not execute this object's member to inspect the returned value. Our executable separately checked that the accepted call produces the maximum representable std::size_t, rather than assuming a particular number of bits.
Use this counterexample to separate three possible repairs. If the API supports only a deliberately narrower family of types, express that family in the declaration. If signed results are supported but negative values are invalid at runtime, validate the value before converting and define the failure behavior. If the supplied type violates a documented semantic contract, explain how tests or the caller's obligations expose that violation. These choices have different compatibility consequences.
Do not immediately strengthen the concept to require exactly std::size_t without discussing the trade-off. That would reject some otherwise usable interfaces while still accepting a dishonest member returning an unsigned value. A return-type constraint can establish a useful syntactic boundary; it cannot independently establish that the object reports its actual element count. An interview answer should state both what the compiler checks and what the program still has to trust or verify.

Why can similar constraints still be ambiguous?
Build a stronger concept by reusing the weaker one:
template<class T>
concept Indexed = Sized<T> && requires(const T& v) { v[0]; };
template<Sized T> int pick(const T&) { return 1; }
template<Indexed T> int pick(const T&) { return 2; }
In the tested C++20 program, a vector selected the second overload and returned 2; a list selected the first and returned 1. The stronger constraint incorporates the same named Sized requirement. The language's constraint ordering rules can recognize the resulting subsumption relationship for these declarations.
Now define two independent concepts whose bodies both contain requires(const T& v) { v.size(); }. Use the first for one overload, and combine the second with an indexing requirement for the other. It is tempting to assume that the second overload must win because it asks for “size plus indexing.” Our negative compile test was ambiguous.
template<class T>
concept HasSize = requires(const T& v) { v.size(); };
template<class T>
concept HasSizeAgain = requires(const T& v) { v.size(); };
template<class T>
concept IndexedAgain = HasSizeAgain<T> && requires(const T& v) { v[0]; };
template<HasSize T> int pick(const T&) { return 1; }
template<IndexedAgain T> int pick(const T&) { return 2; }
// pick(std::vector<int>{1, 2}); // ambiguous in this overload set
The reason is structural: atomic constraint identity depends on the source expression and its parameter mapping. Two separately written checks do not automatically become the same atom because a programmer describes them identically. Subsumption is not an unrestricted mathematical proof that one arbitrary predicate implies another.
In an interview, draw the atoms rather than counting requirements. Show which named concept supplies the shared atom, then add the extra requirement. Reuse the common concept when that relationship is intentional. Do not conclude that any longer constraint always wins; overload ordering also depends on the declarations being compared.
How should you verify and explain a repair?
Before expanding the implementation, list the calls that must compile and the calls that must fail. For these examples, the important cases are a supported container, an unsupported scalar, conflicting argument deductions, and a type satisfying both overload constraints. Test expected failures as well as successful output: silently deleting the fallback test would miss the body-instantiation regression.
| Exercise | Expected evidence |
|---|---|
Same-type versus mixed-type larger | 4 for integers; conflicting deduction for the mixed call. |
Explicit larger<double> | 4.5; explain why conversion now applies. |
| Declaration SFINAE versus body-only check | Fallback -1 versus a compilation error in extent<int>. |
| Named refinement versus independent checks | Deterministic vector/list selection versus ambiguity. |
The local checks used Apple clang 21.0.0 with -Wall -Wextra -pedantic-errors, -std=c++17 for deduction/SFINAE and -std=c++20 for concepts. Three positive fixtures produced the expected output; three separate negative fixtures failed for the expected reasons. This is one compiler observation, not a cross-toolchain compatibility guarantee. Clang's language-status page documents the mode flags and upstream implementation status.
If asked whether if constexpr fixes the problem, first locate the failure. It can select an appropriate branch inside an instantiated template, but it does not itself remove the function from the overload set. Use it when one accepted interface legitimately has different implementations; constrain the declaration when callers should not consider that interface for a type.
What does a concise spoken answer sound like?
For the body-only extent example, try this explanation: “The integer deduces T successfully. The template offers a better argument match than the ellipsis overload. Its deduced return type requires checking the body, where member access on an integer is invalid. That error does not remove the candidate through SFINAE. I would move the supported-operation requirement into the declaration if unsupported types should use the fallback.” This answer identifies both the mechanism and the intended behavior.
Then ask what the caller actually needs. If a scalar should always be rejected, a fallback accepting everything may weaken the interface. If heterogeneous containers are intentional, a named constraint can document the shared operation. If only an implementation detail differs, a trait and if constexpr may be sufficient. The desired contract decides the repair.
Finally, keep the failing call when you refactor. A positive test can establish that a supported vector still works; it cannot establish that an unsupported integer fails at the intended boundary. In a real repository, put negative compilation checks in separate fixtures so they do not prevent the ordinary test suite from building. Match the error category rather than an entire compiler-specific diagnostic string, which can change between toolchain versions.
Practice explaining a template contract
Use these PracHub exercises to move from isolated syntax to decisions you can defend. The focus column gives a concrete follow-up rather than implying that every exercise tests concepts directly.
| PracHub practice question | Template-focused follow-up |
|---|---|
| Implement and Defend a Generic C++ Stack | State element requirements and identify which unsupported operations should fail at the interface. |
| Implement a Simplified std::vector in C++: Growth, Memory, Exception Safety, and Type Traits | Separate a type trait selecting implementation behavior from a requirement excluding an operation. |
| Concatenate Two C++ Index Sequences at Compile Time | Trace the deduced packs and verify the resulting type with a compile-time assertion. |
| Compare C++ Virtual Dispatch with CRTP | Explain where a missing derived operation is diagnosed and how an interface expresses its expectations. |
| Debug a Test-Driven C++ Project | Reduce a compiler failure to one call, one overload set, and one expected outcome. |
For a focused next session, implement the generic C++ stack, then write one accepted and one intentionally rejected use. Explain the rejection stage before editing the implementation. Keep the rejected use alongside the implementation so a later refactor can check the same boundary.
Sources and Further Reading
- C++ working draft: deduction from function calls
- C++ working draft: deduction and substitution, including immediate context
- C++ working draft: requires-expressions
- C++ working draft: convertible_to
- C++ working draft: constrained declarations
- C++ working draft: constraint ordering
- C++ working draft: atomic constraints
- Clang C++ implementation status and language flags
Comments (0)