Non-Deduced Contexts in C++ Template Deduction
Template argument deduction allows the C++ compiler to automatically determine template arguments from the types of function call arguments. However, certain syntactic positions—formally defined as non-deduced contexts—prevent the compiler from deducing these types automatically. This article examines what non-deduced contexts are, how the compiler behaves when encountering them, common scenarios where they occur, and how developers intentionally utilize them to control overload resolution and enforce explicit type specification.
What Is a Non-Deduced Context?
During template argument deduction, the compiler inspects the types of function arguments supplied at a call site and compares them against the parameter types declared in the function template. When a template parameter appears in a construct where deduction cannot deterministically or unambiguously infer the original type, the C++ standard classifies that construct as a non-deduced context.
In these contexts, the compiler does not attempt deduction on that specific parameter. The type must either be deduced from another function argument where the parameter appears in a deducible context, supplied via a default template argument, or provided explicitly at the call site. If the parameter cannot be resolved through any of these means, the compiler issues a deduction failure, causing the template to be removed from the candidate set via SFINAE or resulting in a hard compilation error.
Common Non-Deduced Contexts
The C++ standard (specifically [temp.deduct.type]) enumerates the syntactic forms that create non-deduced contexts. The most common include:
1. Nested-Name-Specifiers (Dependent Scopes)
When a template parameter is qualified with the scope resolution
operator (::), the compiler cannot invert the mapping to
find the enclosing type:
template <typename T>
void inspect(typename T::value_type value);
// Usage:
inspect(42); // Error: T cannot be deduced from typename T::value_typeBecause multiple distinct types could define a member type alias
named value_type with the identical underlying type, the
mapping from T::value_type back to T is not
injective. Therefore, T sits in a non-deduced context.
2. Non-Type Template Parameters Involving Expressions
If a non-type template parameter is part of an expression other than a single, isolated parameter, deduction cannot run backward:
template <int N>
void buffer(char (&arr)[N + 1]);
char data[10];
buffer(data); // Error: N + 1 is an expression, creating a non-deduced context for NThe compiler does not perform algebraic inversion to deduce that
N = 9.
3. Template
Parameters Used in decltype Expressions
Types dependent on expressions inside decltype that
depend on other un-deduced template arguments typically create
non-deduced contexts.
4. Arguments to Pack Expansions in Trailing Positions
When an argument corresponds to a parameter pack that is not at the end of the parameter list, or when multiple expansions interact ambiguously, non-deduced contexts can arise.
How the Deduction Engine Resolves Types
When an argument appears in a non-deduced context, the compiler follows a strict sequence:
- Bypass Deduction: The deduction engine skips examining the function parameter in the non-deduced context.
- Alternative Deduction: The compiler checks the remaining parameters. If the same template parameter appears in a deduced context in another argument, the parameter is deduced from that argument.
- Explicit Specification: If the template parameter
is explicitly provided at the call site (e.g.,
inspect<MyContainer>(42)), the explicit type is substituted. - Consistency Check: Once the template parameter is known (via other arguments or explicit specification), the compiler substitutes the concrete type into the non-deduced context. It then verifies whether the argument passed can implicitly convert to the substituted parameter type. If conversion fails, deduction fails.
template <typename T>
void process(T primary, typename T::value_type secondary);
struct Container {
using value_type = int;
};
Container c;
process(c, 10); // OK: T is deduced from 'c' as Container.
// typename Container::value_type evaluates to int.
// 10 matches int.Deliberate Use: Controlling Overloads and Conversions
Non-deduced contexts are frequently introduced on purpose to prevent deduction on specific function parameters, thereby enabling implicit conversions or resolving ambiguous function overloads.
Preventing Ambiguity in Two-Argument Functions
Consider a min function:
template <typename T>
T min(T a, T b);
min(10, 20.5); // Error: deduced conflicting types for parameter 'T' (int vs double)By placing one parameter into a non-deduced context, deduction only
occurs on the first parameter, while the second parameter accepts any
type convertible to T:
template <typename T>
T min(T a, typename std::type_identity<T>::type b) {
return (b < a) ? b : a;
}
min(10, 20.5); // OK: T is deduced as int from the first argument.
// The second parameter becomes int, and 20.5 is converted to 20.The
std::type_identity Idiom (C++20)
Before C++20, developers manually defined an identity metafunction:
template <typename T>
struct identity {
using type = T;
};C++20 formalized this pattern into <type_traits>
with std::type_identity and the convenience alias
std::type_identity_t:
#include <type_traits>
template <typename T>
void configure(T key, std::type_identity_t<T> fallback);Because std::type_identity_t<T> resolves via
typename std::type_identity<T>::type, it immediately
forms a nested-name-specifier, forcing the compiler to treat
fallback as a non-deduced context.