Замыкания методов без лямбд
В C++ валиден такой синтаксис:
struct A
{
int foo(int x);
};
int bar(int x);
A a;
(a.foo)(5);
(bar)(5);
При этом выражение (bar) может быть присвоено переменной – это указатель на функцию.
auto ptr1 = (bar); // это создаёт функциональный объект (указатель)
ptr1(5); // это вызывает функцию
(a.foo) – это замыкание, реализованное средствами языка так, что время его жизни крайне короткое, не уверен, что это даже считается выражением. В работе [1] вводится термин PMFC (pending member function call). Чтобы продлить время жизни этой конструкции, её нужно завернуть в лямбда-функцию.
auto ptr2 = [&a](int x) { return (a.foo)(x); }
ptr2(5);
Чем больше обобщений требуется, тем больше шаблонов и &&-ов будет содержать лямбда. Многословность можно решить через препроцессор со всеми вытекающими проблемами:
#define CLOSURE(x) ([&]<typename ... Args>(Args&& ... args) { return (x)(std::forward<Args>(args)...); })
// эквивалентный код:
(a.foo)(5);
CLOSURE(a.foo)(5);
// можно разделить на две части:
auto ptr3 = CLOSURE(a.foo);
ptr3(5);
Помимо общих проблем макросов, нужно учесть и специфику лямбд...
A* b;
auto questionable = CLOSURE(b->foo);
++b;
questionable(5); // наш макрос захватывает по ссылке и мы работаем с изменившимся указателем...
Почему бы тогда не задействовать неявный синтаксис, уже наполовину предоставляемый языком, и возложить создание подходящего функционального объекта на компилятор? Сравните с примером ptr1 выше.
auto ptr4 = (a.foo); // это создаёт функциональный объект
ptr4(5); // это вызывает функцию
Для этого в библиотеке нужен такой класс, либо лямбда:
template<typename T, typename R, typename ... Args>
class std::call
{
public:
constexpr std::call(T& ptr, R (T::*mem)(Args...)) : ptr(ptr), mem(mem) {}
R operator()(Args&& ... args) const
{
return (ptr.*mem)(std::forward<Args>(args)...);
}
private:
T& ptr;
R (T::*const mem)(Args...);
};
и способ научить компилятор перетащить аргументы операторов ., ->, .*, ->* в его/её конструктор:
/* (a.foo) */ std::call(a, &std::remove_cvref_t<decltype(a)>::foo);
/* (a->foo) */ std::call(*(a.operator->()), &std::remove_cvref_t<decltype(*(a.operator->()))>::foo);
/* (a.*ptr) */ std::call(a, ptr);
/* (a->*ptr) */ std::call(*a, ptr); // если оператор не перегружен
Возможный вариант — парсить выражение в скобках как std::pair, и уже в таком виде подавать в std::call (либо отнаследовать его от std::pair).
Ссылки:
- https://www.aristeia.com/Papers/DDJ_Oct_1999.pdf
Т.е. у нас нарушается правило эквивалентности (abc) и abc в этом месте ?
Если это является проблемой (мне казалось, что уже есть примеры, когда скобки меняют поведение), то можно и без скобок разрешить:
auto ptr4 = a.foo; // это создаёт функциональный объект
ptr4(5); // это вызывает функцию
Последний пример -- это кажется в точности std::bind. Только pointer to member function в стандартной библиотеке следует вперёди указателя на экземпляр класса обычно (или даже всегда).
Ну тогда лучше использовать bind_front/bind_back , а ещё лучше вместе с NTTP: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2714r0.html