How std::decay Simulates Parameter Passing in C++
In C++, std::decay is a type trait that models the exact
type transformations applied to an expression when it is passed as a
function argument by value. When templates take arguments by forwarding
reference (T&&), they preserve references,
cv-qualifiers, and original composite types like arrays and functions.
Applying std::decay strips these properties, enabling
developers to determine what type an object would become if it were
copied or moved into an ordinary, non-reference parameter.
The Problem of Pass-by-Value Semantics
In standard C and C++, passing an argument by value does not always retain the argument's exact original type. Specifically:
- References are discarded because a copy is being made.
- Top-level
constandvolatilequalifiers are removed because the callee operates on its own independent copy. - Raw arrays cannot be passed by value, so they implicitly convert (decay) into pointers to their first elements.
- Functions cannot be passed as values directly, so they implicitly convert to function pointers.
When writing generic templates using universal references
(Args&&...), the compiler deduces the exact
reference and qualification of the passed arguments. If a template needs
to store, allocate, or wrap these forwarded arguments, working with the
deduced type directly can cause compilation errors or dangling
references. std::decay resolves this by reconstructing the
type as if it were accepted by a traditional by-value signature.
The Three Step Rules of
std::decay
Defined in the <type_traits> header,
std::decay<T>::type (or the alias template
std::decay_t<T>) implements the transformations
outlined in the C++ standard under the rules for lvalue-to-rvalue,
array-to-pointer, and function-to-pointer conversions:
1. Reference and cv-Qualifier Removal
If T is a reference type (such as int&
or const int&), std::decay first removes
the reference using std::remove_reference. Following
reference removal, any top-level const or
volatile specifiers are removed from non-array,
non-function types:
std::decay_t<const int&>becomesintstd::decay_t<volatile double>becomesdoublestd::decay_t<std::string&&>becomesstd::string
2. Array-to-Pointer Conversion
If the underlying type is an array of known or unknown bound,
std::decay converts it into a pointer to the array's
element type, discarding the size:
std::decay_t<int[5]>becomesint*std::decay_t<const char[12]>becomesconst char*std::decay_t<int[]>becomesint*
3. Function-to-Pointer Conversion
If the underlying type is a function type, std::decay
converts it into a pointer to that function signature:
std::decay_t<void(int)>becomesvoid(*)(int)std::decay_t<int()>becomesint(*)()
Practical Use Cases in Modern C++
std::decay is utilized across the standard library
wherever by-value storage of forwarded template arguments is
required:
- Factory Functions: Utilities such as
std::make_pair,std::make_tuple, andstd::make_sharedapply decay to their deduced template arguments. This prevents them from attempting to store uncopyable raw array types or references that could lead to dangling pointers. - Asynchronous Execution: Primitives like
std::threadandstd::asyncstore copies of their arguments to run on different stacks. They applystd::decayto ensure the stored callable and arguments do not unintentionally maintain references to variables in the spawning scope. - Generic Storage Types: Implementations of
type-erased wrappers such as
std::anyorstd::functionrely onstd::decayto normalize target types before allocating storage.