В универсальном адаптере требуется вызвать указатель на метод, не зная заранее, будет ли объект ссылкой или указателем. Какой механизм C++17 следует применить вместо ручного разветвления? Определите вывод программы.
#include <functional>
#include <iostream>
struct Device {
int read() const { return 42; }
};
int main() {
Device device;
auto member = &Device::read;
std::cout << std::invoke(member, device) << '
';
std::cout << std::invoke(member, &device) << '
';
}
Следует использовать std::invoke. Программа выведет два раза 42, потому что std::invoke единообразно вызывает обычные вызываемые объекты, указатели на методы и указатели на поля, корректно обрабатывая объект, ссылку, указатель или std::reference_wrapper.
До C++17 универсальному коду приходилось отдельно учитывать синтаксис вызова обычной функции и указателя на член класса. Для указателя на метод требовалось использовать выражения вида (object.*member)() или (pointer->*member)(), что усложняло шаблонные адаптеры.
В стандартной библиотеке уже существовали средства вроде std::mem_fn и std::bind, однако они создают дополнительные обёртки. std::invoke предоставил единый низкоуровневый механизм вызова, удобный для алгоритмов и шаблонных библиотек.
Указатель на метод нельзя вызвать как обычный объект-функцию: ему нужен объект, для которого выполняется метод. Синтаксис зависит от формы объекта — это может быть объект, указатель на объект или обёртка std::reference_wrapper.
Если универсальный адаптер неправильно обработает эту форму, он либо не скомпилируется, либо будет вынужден содержать несколько ветвей с разными выражениями вызова. Это увеличивает код и риск ошибок в шаблонном интерфейсе.
std::invoke(member, device) эквивалентен вызову метода через объект, а std::invoke(member, &device) — вызову через указатель. В обоих случаях библиотека выбирает нужную форму обращения к указателю на член во время компиляции.
Для обычной функции std::invoke просто выполняет обычный вызов. Для указателя на поле он возвращает соответствующее поле объекта, поэтому механизм применяется не только к методам.
В шаблонном коде std::invoke особенно полезен вместе с std::is_invocable, std::invoke_result или их современными концептами std::invocable и std::invoke_result_t. Важно помнить, что сам std::invoke не управляет временем жизни объекта: переданный указатель или reference_wrapper должны оставаться действительными.
По сравнению с ручным .* или ->* решение на std::invoke короче, обобщённее и соответствует интерфейсам стандартных алгоритмов. Компромисс состоит в том, что вызываемый объект может быть менее очевиден при чтении кода, поэтому в публичном API полезно давать параметру понятное имя или ограничивать его концептом.
Допустим, библиотечный адаптер получает операцию и элемент контейнера. Один вариант — вручную различать указатель на метод, обычную функцию, указатель на объект и std::reference_wrapper. Плюс такого подхода — явность, минус — большое количество условной логики, сложные требования к типам и риск неполного покрытия случаев.
Второй вариант — оборачивать каждую операцию через std::mem_fn или std::bind. Это может упростить вызов метода, но создаёт дополнительный тип-обёртку и хуже сочетается с современными ограничениями шаблонов.
Выбранное решение — вызывать операцию через std::invoke, предварительно ограничив параметр концептом std::invocable. Так адаптер поддерживает стандартный набор вызываемых объектов без ручного разветвления, а ошибки передаются на этап проверки интерфейса. Результат — единая реализация с предсказуемой компиляцией и совместимостью с идиомами стандартной библиотеки.
std::invoke только с указателями на методы?Нет. Он также вызывает обычные функции, функциональные объекты, лямбды и указатели на поля. Для указателя на поле результатом будет обращение к полю, а не вызов функции.
Вызов не будет корректным: тип объекта не соответствует классу, которому принадлежит указатель на член. В шаблонном коде это лучше обнаруживать ограничением std::invocable<Callable, Args...>, чтобы получить осмысленное исключение из перегрузки вместо глубокой ошибки инстанцирования.
std::invoke время жизни объекта, переданного указателем?Нет. std::invoke лишь выбирает и выполняет форму вызова. Если передан висячий указатель или std::reference_wrapper на уничтоженный объект, поведение программы остаётся неопределённым; механизм вызова не добавляет владения и не выполняет проверок времени жизни.