How std::invoke_result Deduces Return Types in C++

Introduced in C++17 to replace std::result_of, std::invoke_result is a type trait that deduces the return type of any callable entity when invoked with a specific set of arguments at compile time. This article explains the internal mechanics of std::invoke_result, detailing how it leverages the standardized INVOKE concept, unevaluated contexts, std::declval, and decltype to compute return types without generating runtime overhead or requiring callable objects to be instantiated.

The Foundation: The INVOKE Concept

In C++, "callables" are diverse. They include:

  • Free functions and function pointers
  • Functors (classes with an operator())
  • Lambdas
  • Member function pointers
  • Member data pointers

Before C++17, determining the return type of arbitrary callables was inconsistent because member pointers require an object instance ((obj.*ptr)() or (ptrObj->*ptr)()), whereas regular functions use the standard call syntax (func()).

std::invoke_result addresses this by relying on the standard INVOKE<R>(f, args...) rules defined by the C++ specification. The standard library provides std::invoke as the direct implementation of these rules, unifying call syntax across all callable types.

Compile-Time Deduction via Unevaluated Contexts

At the core of std::invoke_result is the unevaluated operand. An unevaluated operand is an expression that the compiler analyzes solely for its type and value category, without compiling it into machine instructions or executing it.

std::invoke_result uses two primary compile-time utilities inside this unevaluated context:

  1. std::declval<T>(): Generates a pseudo-value of type T at compile time without constructing an actual object. This allows deduction on types that lack default constructors or are abstract.
  2. decltype(...): Inspects the declared type of the resulting expression.

A conceptual implementation of std::invoke_result looks like this:

template <typename F, typename... Args>
struct invoke_result {
    using type = decltype(std::invoke(std::declval<F>(), std::declval<Args>()...));
};

template <typename F, typename... Args>
using invoke_result_t = typename invoke_result<F, Args...>::type;

When you pass types to std::invoke_result<F, Args...>, the compiler simulates calling std::invoke using fictional instances created by std::declval. The decltype specifier queries the exact type produced by this simulated call.

Handling Incompatible Arguments with SFINAE

A critical requirement for modern type traits is that invalid combinations of callable objects and arguments must not trigger hard compiler errors. Instead, they must result in a substitution failure (SFINAE - Substitution Failure Is Not An Error).

Standard library implementations achieve this by specializing a base template:

// Primary template: inactive / invalid call
template <typename AlwaysVoid, typename F, typename... Args>
struct invoke_result_impl {};

// Partial specialization: enabled only if std::invoke is valid
template <typename F, typename... Args>
struct invoke_result_impl<
    std::void_t<decltype(std::invoke(std::declval<F>(), std::declval<Args>()...))>,
    F, Args...
> {
    using type = decltype(std::invoke(std::declval<F>(), std::declval<Args>()...));
};

template <typename F, typename... Args>
struct invoke_result : invoke_result_impl<void, F, Args...> {};
  • If std::invoke(std::declval<F>(), std::declval<Args>()...) is well-formed, the specialization matches, and member alias type is defined.
  • If the call is invalid (e.g., mismatched argument count or incompatible types), the specialization is discarded due to SFINAE, leaving invoke_result without a type member.

This failure mechanism enables boolean detection traits such as std::is_invocable and std::is_invocable_r, which simply check whether std::invoke_result<F, Args...>::type exists.

Member Pointers and References

std::invoke_result also correctly deduces value categories and references:

  • Lvalue references: If a function returns an lvalue reference (e.g., int&), std::invoke_result evaluates to int&.
  • Rvalue references: Functions returning T&& evaluate to T&&.
  • Member object pointers: Accessing a member variable int MyClass::* on an object MyClass returns a reference to that member type (int& or const int& depending on the object's constness).

Because it routes through the INVOKE rules rather than treating callables strictly as function invocations, std::invoke_result consistently resolves the precise return type across all callable entities in modern C++.