How std::invoke Unifies Calling in C++
Introduced in C++17, std::invoke provides a standardized
mechanism to execute any callable entity using a single, unified
interface. Before its introduction, generic programming in C++ required
different syntaxes depending on whether you were calling a regular
function, invoking a member function pointer, or accessing a member
variable pointer. This article explores how std::invoke
eliminates these syntactic disparities, how it operates behind the
scenes, and how to apply it across different C++ callable types.
The Problem: Syntactic Divergence in C++
Prior to C++17, writing generic template code that accepted any callable object was cumbersome because C++ enforces distinct syntactical rules based on the callable's type:
Free functions, functors, and lambdas use the standard function call syntax:
func(arg1, arg2);Pointers to member functions require a class instance or pointer, dereferencing operators (
.*or->*), and explicit grouping parentheses due to operator precedence:(instance.*ptrToMemberFunc)(arg1, arg2); (instancePtr->*ptrToMemberFunc)(arg1, arg2);Pointers to data members access values without invocation parentheses:
instance.*ptrToDataMember; instancePtr->*ptrToDataMember;
Because of these differences, generic code that took an arbitrary callable had to provide complex template specializations or utilize helper traits to handle member pointers properly.
How
std::invoke Resolves the Disparity
Defined in the <functional> header,
std::invoke implements the concept of INVOKE
expressions defined in the C++ standard. It uses template deduction and
SFINAE (or concepts in C++20) to inspect the first argument and
determine the correct invocation syntax at compile time.
The general syntax is uniform:
std::invoke(callable, target_or_arg1, args...);Depending on the type of callable,
std::invoke automatically routes the arguments to the
proper underlying language expression.
1. Free Functions, Function Pointers, and Lambdas
For standard callables, std::invoke passes the trailing
arguments directly to the function call operator:
#include <iostream>
#include <functional>
void printSum(int a, int b) {
std::cout << a + b << '\n';
}
int main() {
auto lambda = [](int x) { return x * 2; };
std::invoke(printSum, 10, 20); // Equivalent to: printSum(10, 20)
int result = std::invoke(lambda, 5); // Equivalent to: lambda(5)
}2. Pointers to Member Functions
When the first argument is a pointer to a member function,
std::invoke treats the second argument as the class
instance and the remaining arguments as the function parameters. It
automatically handles instances passed by reference, by value, by
pointer, or via smart pointers:
#include <iostream>
#include <functional>
#include <memory>
struct Calculator {
void add(int a) const {
std::cout << "Value: " << a << '\n';
}
};
int main() {
Calculator calc;
auto calcPtr = std::make_unique<Calculator>();
// Using an object reference
std::invoke(&Calculator::add, calc, 42);
// Using a smart pointer (automatically dereferenced)
std::invoke(&Calculator::add, calcPtr, 42);
}This removes the need to manually write
(calc.*&Calculator::add)(42) or check if the target is
a raw pointer or smart pointer.
3. Pointers to Data Members
In modern C++, pointers to data members are categorized as callables. Invoking a pointer to a member variable retrieves a reference to that member from the target instance:
#include <iostream>
#include <functional>
struct User {
std::string name;
};
int main() {
User user{"Alice"};
// Accessing member variable
std::cout << std::invoke(&User::name, user) << '\n'; // Equivalent to: user.name
// Modifying member variable through returned reference
std::invoke(&User::name, user) = "Bob";
std::cout << user.name << '\n'; // Outputs: Bob
}Generic Programming Applications
The greatest utility of std::invoke lies in template and
library design. A generic utility such as a thread pool, an asynchronous
wrapper, or an algorithm can accept any callable without writing
specialized overloads:
template <typename Callable, typename... Args>
decltype(auto) execute(Callable&& callable, Args&&... args) {
// Executes correctly whether Callable is a free function, lambda, or member pointer
return std::invoke(std::forward<Callable>(callable), std::forward<Args>(args)...);
}Along with std::invoke, C++17 introduced accompanying
type traits:
std::invoke_result<F, Args...>(and its aliasstd::invoke_result_t): Determines the return type of the invocation at compile time.std::is_invocable<F, Args...>: Tests whether a callable type can be invoked with the specified argument types.
Together, these utilities ensure that any callable object in C++ can be examined, checked, and executed through a predictable and consistent interface.