std::type_identity and Non-Deduced Contexts in C++
Introduced in C++20, std::type_identity is a fundamental
utility type trait designed to return its argument type unmodified,
primarily serving to create non-deduced contexts during template
argument deduction. This article explores the purpose of
std::type_identity, demonstrates how the C++ template
deduction engine handles nested types, and illustrates how developers
can leverage this trait to prevent deduction conflicts and enforce
implicit conversions.
What is std::type_identity?
Defined in the <type_traits> header,
std::type_identity is an identity type transformer. Its
conceptual implementation is minimal:
namespace std {
template<class T>
struct type_identity {
using type = T;
};
template<class T>
using type_identity_t = typename type_identity<T>::type;
}Although it performs no modifications to T, its value
lies in how the C++ compiler processes template parameters wrapped
inside it.
Understanding Non-Deduced Contexts
Template argument deduction allows the compiler to automatically determine template parameters based on the arguments passed to a function call. However, according to the C++ standard (specifically [temp.deduct.type]), certain syntactic positions are classified as non-deduced contexts.
A non-deduced context occurs when a template parameter appears to the
left of the scope resolution operator (::), such as in a
nested type:
typename SomeTrait<T>::typeThe compiler cannot reliably deduce T by inspecting
SomeTrait<T>::type because any number of explicit
specializations of SomeTrait could theoretically map
different types to the same inner type alias. Because
reverse-lookup from type to T is
mathematically non-injective, the language specification mandates that
the compiler must skip deduction for any parameter placed in this
position.
The Purpose of std::type_identity
By placing a template parameter inside
std::type_identity_t<T>, you explicitly wrap it in a
nested type alias
(typename std::type_identity<T>::type), intentionally
moving that parameter into a non-deduced context.
This pattern solves two primary challenges in modern C++:
1. Resolving Deduction Ambiguity and Enabling Implicit Conversions
Consider a template function with two parameters of the same type:
template <typename T>
T add(T a, T b) {
return a + b;
}
// Usage:
add(4.2, 2); // Compilation Error: deduced conflicting types for parameter 'T' ('double' vs 'int')Because both parameters participate in deduction, passing a
double and an int causes a conflict. Using
std::type_identity_t on the second parameter excludes it
from deduction:
template <typename T>
T add(T a, std::type_identity_t<T> b) {
return a + b;
}
// Usage:
add(4.2, 2); // OK: T is deduced solely from 'a' as double; '2' implicitly converts to double.Here, T is deduced exclusively as double
from the first argument. The second argument’s expected type is resolved
to double, allowing the int value
2 to implicitly convert without template deduction
errors.
2. Forcing Explicit Template Arguments
std::type_identity can also be used to disable template
deduction completely, forcing callers to explicitly specify the template
type:
template <typename T>
void process(std::type_identity_t<T> value);
// Usage:
process(42); // Compilation Error: T cannot be deduced
process<int>(42); // OK: T is explicitly specifiedThis prevents subtle bugs caused by unintentional type deduction, ensuring callers make deliberate choices regarding types.