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:
std::declval<T>(): Generates a pseudo-value of typeTat compile time without constructing an actual object. This allows deduction on types that lack default constructors or are abstract.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 aliastypeis defined. - If the call is invalid (e.g., mismatched argument count or
incompatible types), the specialization is discarded due to SFINAE,
leaving
invoke_resultwithout atypemember.
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_resultevaluates toint&. - Rvalue references: Functions returning
T&&evaluate toT&&. - Member object pointers: Accessing a member variable
int MyClass::*on an objectMyClassreturns a reference to that member type (int&orconst 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++.