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:

  1. 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.
  2. 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 T is impossible.
  • Preventing Undefined Behavior: If std::declval returned a fabricated reference at runtime (for example, by dereferencing a nullptr), calling members on it would trigger immediate undefined behavior. Preventing compilation entirely ensures safety.
  • Zero Runtime Overhead: By constraining std::declval strictly 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.