cpp-ru / ideas

Идеи по улучшению языка C++ для обсуждения
https://cpp-ru.github.io/proposals
Creative Commons Zero v1.0 Universal
89 stars 0 forks source link

Замыкания методов без лямбд #530

Open pavelkryukov opened 1 year ago

pavelkryukov commented 1 year ago

В 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).

Ссылки:

  1. https://www.aristeia.com/Papers/DDJ_Oct_1999.pdf
NN--- commented 1 month ago

Т.е. у нас нарушается правило эквивалентности (abc) и abc в этом месте ?

pavelkryukov commented 1 month ago

Если это является проблемой (мне казалось, что уже есть примеры, когда скобки меняют поведение), то можно и без скобок разрешить:

auto ptr4 = a.foo; // это создаёт функциональный объект 
ptr4(5); // это вызывает функцию
tomilov commented 1 month ago

Последний пример -- это кажется в точности std::bind. Только pointer to member function в стандартной библиотеке следует вперёди указателя на экземпляр класса обычно (или даже всегда).