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

  1. Unevaluated Contexts Only: Calling std::declval in an evaluated context (runtime code) produces a static assertion failure or a linker error because no implementation exists.
  2. Abstract Classes: std::declval works with abstract classes and incomplete types, as long as the member access itself does not require a complete type where prohibited.
  3. Reference Types: If T is void, std::declval<void>() returns void, making it resilient in generic templates where T may occasionally resolve to void.