std::declval: Testing Types Without Default Constructors
In modern C++, metaprogramming techniques such as SFINAE and C++20
concepts frequently inspect the properties, member variables, and member
functions of types at compile time. However, types that lack a default
constructor cannot be instantiated using expressions like
T().member, causing compile-time errors even when the
member exists. This article explains the role of
std::declval<T>() in bypassing the need for object
construction, allowing developers to safely test member accessibility
and expression validity purely within unevaluated contexts.
The Problem: Inspecting Non-Default-Constructible Types
When writing type traits or concept constraints, you often need to
determine whether a class T contains a specific member
function or field. A naive approach to verifying a member function
foo() might look like this:
template <typename T>
using has_foo_t = decltype(T().foo());This implementation assumes T can be value-initialized
via T(). If T deletes its default constructor,
requires parameters, or has private constructors, T()
causes a compilation error regardless of whether T::foo()
exists. Because the expression relies on object creation rather than
solely inspecting the type's interface, it fails to work generically
across all possible types.
How std::declval Solves the Problem
Introduced in C++11 and defined in <utility>,
std::declval provides a fictitious reference to an object
of type T without actually creating one. Its signature is
conceptually defined as:
template <typename T>
typename std::add_rvalue_reference<T>::type declval() noexcept;std::declval is declared but intentionally never
defined. Because it has no definition, it can only be utilized inside
unevaluated operands, such as within decltype,
sizeof, noexcept, or requires
clauses. Within these unevaluated contexts, the compiler does not
generate code to execute the function or construct an object; it only
needs to know the type that would be returned.
By returning an rvalue reference (T&&),
std::declval<T>() allows you to simulate member
access as if an instance of T were available.
Testing Member Accessibility in Practice
Using std::declval, compile-time queries evaluate only
the interface of the class rather than its constructibility.
Pre-C++20 (SFINAE / void_t)
Using std::void_t and std::declval, you can
detect if a type has an accessible member function
process() even if the type cannot be
default-constructed:
#include <utility>
#include <type_traits>
template <typename, typename = void>
struct has_process : std::false_type {};
template <typename T>
struct has_process<T, std::void_t<decltype(std::declval<T>().process())>>
: std::true_type {};
class NonDefaultConstructible {
public:
NonDefaultConstructible(int x) {}
void process() {}
};
static_assert(has_process<NonDefaultConstructible>::value, "process() should be detected");In this example, std::declval<T>() produces an
rvalue reference to NonDefaultConstructible, allowing
decltype to verify that .process() is
accessible and valid without invoking
NonDefaultConstructible(int).
C++20 Concepts
With C++20 concepts, std::declval can still be used
inside requires expressions, though a parameter list in the
requirement block is often preferred:
template <typename T>
concept ProcessableWithDeclval = requires {
std::declval<T>().process();
};
// Alternative C++20 syntax using a dummy parameter:
template <typename T>
concept ProcessableWithParam = requires(T t) {
t.process();
};Even in C++20, std::declval remains essential when
composing traits outside of standard requires blocks or
when dealing with complex reference-forwarding types.
Important Constraints
- Unevaluated Contexts Only: Calling
std::declvalin an evaluated context (runtime code) produces a static assertion failure or a linker error because no implementation exists. - Abstract Classes:
std::declvalworks with abstract classes and incomplete types, as long as the member access itself does not require a complete type where prohibited. - Reference Types: If
Tisvoid,std::declval<void>()returnsvoid, making it resilient in generic templates whereTmay occasionally resolve tovoid.