библиотека — вызов Callablelibrary — calling a Callable

std::invoke / std::apply

Единообразный вызов чего угодно вызываемого — функции, лямбды, указателя на метод, указателя на поле — и вызов функции с аргументами, уже собранными в tuple.A uniform way to call anything callable — a function, a lambda, a member-function pointer, a data-member pointer — and to call a function with arguments already packed into a tuple.

generic library codegeneric library code частый вопрос: чем invoke отличается от обычного вызоваcommon question: how invoke differs from an ordinary call

Что этоWhat it is

std::invoke(f, args...) вызывает f с аргументами args... независимо от того, что такое f: обычная функция или функциональный объект — вызов эквивалентен f(args...); указатель на функцию-член класса (&Class::method) — первым аргументом ожидается объект или указатель на объект этого класса, дальше обычные аргументы метода; указатель на данные-член (&Class::field) — с единственным аргументом-объектом возвращает значение этого поля. std::apply(f, tuple) вызывает f с аргументами, распакованными из tuple по порядку: apply(f, std::make_tuple(1, 2)) эквивалентно f(1, 2).std::invoke(f, args...) calls f with arguments args... regardless of what f is: an ordinary function or a function object — the call is equivalent to f(args...); a pointer to a class member function (&Class::method) — the first argument is expected to be an object or a pointer to an object of that class, followed by the method's ordinary arguments; a pointer to a data member (&Class::field) — with a single object argument it returns that field's value. std::apply(f, tuple) calls f with arguments unpacked from tuple in order: apply(f, std::make_tuple(1, 2)) is equivalent to f(1, 2).

до C++17 — отдельный синтаксис для member pointerbefore C++17 — separate syntax for a member pointer
struct Widget { void draw() const; };

void callDraw(void(Widget::*m)() const, const Widget& w) {
    (w.*m)();  // синтаксис .* — несовместим с обычным m(w)// .* syntax — incompatible with an ordinary m(w)
}
// generic-код, принимающий "что угодно вызываемое",// generic code accepting "anything callable"
// не мог единообразно обработать этот случай без ручных проверок// couldn't handle this case uniformly without manual checks
C++17 — std::invokeC++17 — std::invoke
struct Widget { void draw() const; };

template <class F, class... Args>
void callAnything(F&& f, Args&&... args) {
    std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
}
callAnything(&Widget::draw, w);  // работает: invoke сам понимает, что это member pointer// works: invoke figures out it's a member pointer itself
callAnything([]{ std::cout << "hi"; });  // тоже работает — обычная лямбда// also works — an ordinary lambda

Зачем ввелиWhy it was introduced

01

Generic-код не мог единообразно поддержать member pointerGeneric code couldn't uniformly support a member pointer

Шаблонная функция, принимающая параметр F как "что угодно вызываемое", до invoke не могла просто написать f(args...) — если вызывающий код передавал указатель на функцию-член, требовался отдельный синтаксис (obj.*f)(args) или (obj_ptr->*f)(args). Без if constexpr/type traits на каждый случай применения такой generic-код не мог обработать оба варианта одной строкой.A template function taking a parameter F as "anything callable" couldn't, before invoke, simply write f(args...) — if the caller passed a pointer to a member function, a separate (obj.*f)(args) or (obj_ptr->*f)(args) syntax was required. Without an if constexpr/type-trait check for every use case, such generic code couldn't handle both variants in one line.

02

Стандартная библиотека уже неявно использовала эту семантикуThe standard library already used this semantics implicitly

std::function, std::thread и std::bind под капотом всегда умели принимать и обычные функции, и member pointers, и вызывали их единообразно — но до C++17 эта "INVOKE-семантика" была описана в стандарте абстрактно, как требование к их реализации, без публичной функции, которую мог бы использовать и прикладной generic-код. std::invoke сделал этот механизм явным и переиспользуемым.std::function, std::thread, and std::bind had always been able under the hood to accept both ordinary functions and member pointers and call them uniformly — but before C++17 this "INVOKE semantics" was described in the standard abstractly, as a requirement on their implementation, without a public function that application-level generic code could also use. std::invoke made this mechanism explicit and reusable.

03

apply закрывает разрыв между tuple и вызовом функцииapply closes the gap between a tuple and a function call

Аргументы для вызова часто оказываются уже собранными как tuple — например, из structured bindings, из variadic-template forwarding, или как результат другой функции. До apply развернуть tuple в отдельные аргументы вызова можно было только вручную через std::index_sequence и рекурсивный шаблон — apply делает это одной строкой.The arguments for a call often turn out to already be packed as a tuple — say, from structured bindings, from variadic-template forwarding, or as the result of another function. Before apply, unpacking a tuple into separate call arguments could only be done by hand via std::index_sequence and a recursive template — apply does it in one line.

Как это работаетHow it works

#include <functional>
#include <tuple>

struct Point {
    int x, y;
    int sum() const { return x + y; }
};

Point p{3, 4};

// invoke с обычной функцией — как обычный вызов:// invoke with an ordinary function — like an ordinary call:
std::invoke(printf, "%d\n", 42);

// invoke с указателем на метод — объект вторым аргументом:// invoke with a member-function pointer — the object is the second argument:
int s = std::invoke(&Point::sum, p);          // эквивалент p.sum()// equivalent to p.sum()

// invoke с указателем на поле — тоже объект вторым аргументом:// invoke with a data-member pointer — also the object as the second argument:
int xv = std::invoke(&Point::x, p);            // эквивалент p.x// equivalent to p.x

// apply — вызов с аргументами, распакованными из tuple:// apply — a call with arguments unpacked from a tuple:
auto args = std::make_tuple(1, 2, 3);
int total = std::apply([](int a, int b, int c) { return a + b + c; }, args);

std::invoke редко пишут напрямую в прикладном коде — это низкоуровневая утилита, на которой строятся другие generic-компоненты. Если в коде встречается прямой вызов std::invoke, скорее всего это реализация чего-то вроде обёртки над Callable, кастомного std::function-подобного типа или generic-адаптера, а не типичный бизнес-код.std::invoke is rarely written directly in application code — it's a low-level utility other generic components are built on. If direct use of std::invoke shows up in code, it's most likely the implementation of something like a wrapper over a Callable, a custom std::function-like type, or a generic adapter, not typical business code.

ГраблиGotchas

Порядок аргументов: указатель на член — первым, объект — вторымArgument order: the member pointer first, the object second

std::invoke(&Point::sum, p), а не std::invoke(p, &Point::sum) — объект передаётся вторым аргументом, что противоречит интуиции обычного вызова метода p.sum(), где объект стоит первым. Перепутать порядок — частая ошибка при первом знакомстве с этой фичей.std::invoke(&Point::sum, p), not std::invoke(p, &Point::sum) — the object is passed as the second argument, which contradicts the intuition of an ordinary method call p.sum(), where the object comes first. Mixing up the order is a common mistake when first encountering this feature.

invoke — не то, что пишут в прикладном коде каждый деньinvoke isn't something written in application code every day

Встретив std::invoke в чужом коде, стоит воспринимать это как сигнал "это generic library code", а не типичная бизнес-логика — обычный вызов функции или лямбды пишется как f(args...) напрямую, invoke нужен именно тогда, когда тип f неизвестен на этапе написания шаблона.Encountering std::invoke in someone else's code should be read as a signal that "this is generic library code," not typical business logic — an ordinary function or lambda call is written as f(args...) directly; invoke is needed precisely when the type of f is unknown at the point the template is written.

apply требует tuple-like контейнер, не произвольную коллекциюapply requires a tuple-like container, not an arbitrary collection

Второй аргумент apply должен поддерживать tuple-протокол (std::tuple_size, get<N>) — обычный std::vector или динамический массив сюда не подходит, потому что число и типы элементов должны быть известны на этапе компиляции. Для runtime-известного числа аргументов apply не применим в принципе.apply's second argument must support the tuple protocol (std::tuple_size, get<N>) — an ordinary std::vector or dynamic array doesn't fit here, because the number and types of elements must be known at compile time. For a runtime-known number of arguments, apply doesn't apply at all.