Why std::declval Is Restricted to Unevaluated Contexts
In C++, std::declval allows developers to refer to
member functions or properties of a type without needing to construct an
instance of that type. However, it can only be used in unevaluated
contexts—such as the operands of decltype,
sizeof, and noexcept—because it has no actual
definition. This article explains the technical reasons behind this
restriction, how the lack of an implementation protects runtime
integrity, and why this design choice is essential for modern template
metaprogramming.
The Purpose of
std::declval
When writing generic template code, you often need to inspect the
return type of an expression involving an arbitrary type T.
For example, you might want to know the return type of
T().do_something().
If T does not have a public default constructor, has
deleted constructors, or is an abstract class, writing
T().do_something() will fail to compile. To solve this, C++
introduces std::declval<T>(), declared in
<utility>, which conceptually produces an rvalue
reference to T (T&&) without invoking
any constructor:
template<typename T>
typename std::add_rvalue_reference<T>::type declval() noexcept;The Technical Limitation: No Definition Exists
The primary reason std::declval cannot be used in
evaluated contexts is that it intentionally lacks a function
definition.
In C++, any function that is evaluated at runtime must be defined. If
a function is odr-used (meaning its address is taken or it is
called in executable code), the compiler and linker require an
implementation to generate executable machine instructions. Because
std::declval is merely declared and never defined by the
standard library:
- In an evaluated context, calling
std::declval<T>()results in a linker error (e.g.,undefined reference to std::declval). In modern C++ implementations, static assertions may also trigger an immediate compile-time error if evaluation is attempted. - In an unevaluated context (such as
decltype(std::declval<T>().foo())), the compiler only computes the static type of the expression. No assembly instructions are emitted, the function is never odr-used, and no reference to a compiled symbol is required.
Design Reasons Behind the Restriction
The standard committee deliberately designed
std::declval to be non-callable at runtime for several key
reasons:
- Impossible Object Creation: Many types cannot be
legally created without runtime parameters, such as abstract classes,
types with private constructors, or types with deleted constructors.
Providing a real runtime value for an arbitrary type
Tis impossible. - Preventing Undefined Behavior: If
std::declvalreturned a fabricated reference at runtime (for example, by dereferencing anullptr), calling members on it would trigger immediate undefined behavior. Preventing compilation entirely ensures safety. - Zero Runtime Overhead: By constraining
std::declvalstrictly to compile-time type deduction, the standard ensures that template metaprogramming utilities produce zero runtime overhead and zero binary footprint.
Summary of Unevaluated Contexts
std::declval is legally valid only within operands of
C++ operators that do not evaluate their arguments:
decltype(...)— Queries the type of an expression.sizeof(...)— Computes the size in bytes of an expression's result type.noexcept(...)— Checks whether an expression can throw an exception.typeid(...)— Queries runtime type information (when the operand is not a polymorphic lvalue).requires (...)— Evaluates concepts and constraints in C++20.
Outside of these constructs, any attempt to invoke
std::declval will trigger a compilation or linking error,
ensuring that non-existent objects are never referenced during program
execution.