перегрузки с приоритетами
От: Кодт Россия  
Дата: 29.06.23 22:56
Оценка: 2 (1)
https://gcc.godbolt.org/z/Kvq6dEGTf

Задумался, как можно было бы реализовать такую штуковину...

Если среди множества перегрузок функции есть несколько одинаково хорошо подходящих, то компилятор выдаёт ошибку неоднозначности.

Один из способов разрешения неоднозначности — это указание приоритета.
Выглядеть это может примерно так:

int fun(prio<0>, auto&&...) { return __LINE__; }

int fun(prio<5>, int) { return __LINE__; }
int fun(prio<5>, const char*) { return __LINE__; }

int fun(prio<10>, int&) { return __LINE__; }
int fun(prio<10>) { return __LINE__; }  // перегрузка с другим количеством аргументов

//int fun(prio<20>, short) { return __LINE__; }
int fun(prio<20>, short&) { return __LINE__; }
int fun(prio<20>, long&) { return __LINE__; }

int fun(prio<30>, int, int) { return __LINE__; }

DEFINE_DISPATCH(do_fun, fun)

#include <iostream>

#define TEST(expr)  std::cout << #expr << " = " << (expr) << std::endl;

int main() {
    TEST(do_fun());
    TEST(do_fun(1));
    TEST(do_fun(""));
    int x;
    TEST(do_fun(x));
    long y;
    TEST(do_fun(y));
    float z;
    TEST(do_fun(z));
    TEST(do_fun(1, 2));
    short t;
    TEST(do_fun(t));
}


По ссылке — моя частично успешная попытка.
По каждой из "частично" заведу отдельные комментарии ниже.
Перекуём баги на фичи!
Re: перегрузки с приоритетами
От: Кодт Россия  
Дата: 29.06.23 22:57
Оценка:
Что мне в ней не нравится концептуально:
1) если есть несколько подходящих перегрузок с разными приоритетами, и у той, что подходит лучше (меньшее количество преобразований типа) приоритет ниже, — до неё дело не дойдёт
2) если есть несколько подходящих перегрузок с одним приоритетом, — ошибка неоднозначности трактуется как неудача подстановки
3) наличие/отсутствие ограничений (concept, requires) не пробрасывается из семейства перегрузок в полиморфную лямбду и может создавать неоднозначность (и см. пункт 2)
Перекуём баги на фичи!
Re: перегрузки с приоритетами
От: Кодт Россия  
Дата: 29.06.23 23:08
Оценка:
Что мне не нравится технически:
Выбор приоритета можно делать разными способами.
// тупо рекурсией

auto dispatch(auto f, auto p, auto... xs) {
  if constexpr(requires{ f(p, xs...); }) return f(p, xs...);
  else if constexpr(p.value > prio_min) return dispatch(f, prio<p.value-1>{}, xs...);
  else static_assert(false, "нешмогла!");
}

// сперва найти приоритет, затем подставить именно его

auto lookup(auto f, auto p, auto... xs) {
  if constexpr(requires{ f(p, xs...); }) return p;
  else if constexpr(p.value > prio_min) return lookup(f, prio<p.value-1>{}, xs...);
  else static_assert(false, "нешмогла!");
}
auto dispatch(auto f, auto... xs) {
  auto p = lookup(f, prio<prio_max>{}, xs...);
  return f(p, xs...);
}

// с учётом того, что от p нам нужен только тип,
auto dispatch(auto f, auto... xs) {
  using P = decltype(lookup(f, prio<prio_max>{}, xs...));  // функции lookup не вычисляются
  return f(P{}, xs...);
}

// с учётом того, что воплощения не нужны,
template<class F, class P, class... Xs> struct Lookup {
  static constexpr bool accepted = requires { std::declval<F>()(P{}, std::declval<Xs>()...); };
  // ну и дальше рекурсия на шаблонах типов... мне что-то лень вдруг стало
};


Я сделал вторым способом — на функциях.
Код достаточно компактный, но. Функции воплощаются и захламляют объектный код, линкеру потом разгребать.
По крайней мере, godbolt показал гору ассемблера.

Рекурсия на шаблонах классов захламит только оперативную память компилятора.
А вот можно ли как-то исхитриться, чтобы минимизировать захламление вообще?

И ещё. if constexpr в сочетании с практикой SFINAE повлекло за собой необходимость всегда возвращать значение приоритета. А это — потребовало ввести значение "ошибочный приоритет".
Как сделать красиво и чтобы функция из тотальной превратилась в частичную, у меня фантазия кончилась. Некрасиво сделать можно, но там будет много чего вытащено в сигнатуры, мясом наружу.
Перекуём баги на фичи!
Re: перегрузки с приоритетами
От: sergii.p  
Дата: 30.06.23 08:45
Оценка:
Здравствуйте, Кодт, Вы писали:

круто. Только ещё бы понять зачем
Вот в некоторых языках вовсе запрещают перегрузки — наверное боятся чего-то (или лучше сказать кого-то). А писать код, который мало того что рассчитывает на правила перегрузки, но ещё и смотрит приоритет... Это ж один напишет код, отладит и уволится. За ним придёт новичок, будет фиксить баг. Посмотрит: приоритеты какие-то. Дай-ка поменяю, авось заработает.
Re[2]: перегрузки с приоритетами
От: Кодт Россия  
Дата: 30.06.23 09:37
Оценка:
Здравствуйте, sergii.p, Вы писали:

SP>круто. Только ещё бы понять зачем


Академический интерес.
Задумался о природе и о способах практического разрешения неоднозначностей (т.е. — как хочет программист, а не как навязывает компилятор из коробки), причём это касается разных языков, не только плюсов.

Как управлять вселенной, не привлекая внимания санитаров влиять на семантику языка, не переписывая компилятор.

На том же питоне, например, можно было бы строить полиморфные функции с нетривиальной диспетчеризацией, просто оббежав всю коллекцию (или, например, граф суперклассов), прочитать аннотации и реализовать абсолютно любую логику.
Хорошо, когда рефлексия есть. А ещё лучше, когда есть запуск пользовательского кода в компиляторе — макросы, вот это вот всё.
А когда нет?

У С++ нет рефлексии, но есть очень затейливые возможности по метапрограммированию.
Тем и интересна эта задача.
Проиллюстрировать теорию на практике.

SP>Вот в некоторых языках вовсе запрещают перегрузки — наверное боятся чего-то (или лучше сказать кого-то).


Или руки не из того места растут.
Можно вспомнить несчастный ML, с его хиндли-милнером, прибитым гвоздями, — и как потом во всех его потомках — что в OcaML, что в F#, разными способами прикручивали ад-хок полиморфизмы.
Даже арифметика там не перегружена, (1 + 2) против (1.0 +. 2.0)

SP>А писать код, который мало того что рассчитывает на правила перегрузки, но ещё и смотрит приоритет...


И мы знаем такие примеры.
В парсерах ряда языков можно и нужно задавать приоритеты (пользовательских) инфиксных операторов.
А с другой стороны, в некоторых языках (семейство APL, семейство SmallTalk) все операторы имеют один приоритет, да ещё и ассоциативность может удивить.

SP>Это ж один напишет код, отладит и уволится. За ним придёт новичок, будет фиксить баг. Посмотрит: приоритеты какие-то. Дай-ка поменяю, авось заработает.


Программирование методом научного тыка — это увлекательно.
Для опытного новичка даже приоритеты не нужны, он просто натыкает поправочных констант плюс-минус-1 по всему коду и авось заработает.
Перекуём баги на фичи!
Re[3]: перегрузки с приоритетами
От: kov_serg Россия  
Дата: 30.06.23 12:04
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Академический интерес.

К>Программирование методом научного тыка — это увлекательно.
А теперь обратная задача выяснить кого вызовут и где оно объявлено
Re: перегрузки с приоритетами
От: Кодт Россия  
Дата: 30.06.23 13:32
Оценка:
Ещё одна концептуальная беда — это невозможность (?) вытащить сигнатуру изнутри наружу.

Покажу на дистиллированном примере.
Пусть у нас есть 2 семейства функций, которые мы хотим склеить.
void f(int&);
void f(const int&);

void g(short&);
void g(long&);

Делаем фасад
void h(auto&& x) -> decltype(f(std::forward<x>(x))) { return f(std::forward<x>(x)); }  // hf
void h(auto&& x) -> decltype(g(std::forward<x>(x))) { return g(std::forward<x>(x)); }  // hg

Эти два шаблона говорят: "если аргумент x можно передать в f (или g, соответственно), — то давайте передадим".
Разумеется, каждый из шаблонов выбирает наилучшую подходящую перегрузку. Но наружу них обоих торчит всего-навсего
template<class T> void h(T&&)

а вовсе не
(int&), (const int&), (short&), (long&)


Попробуем передать
f(1);   // (const int&)
g(1);   // ошибка - ничего не подошло
h(1);   // hf(int) --> f(const int&)

int i;
f(i);   // (int&)
g(i);   // ошибка
h(i);   // hf(int&) --> f(int&)

short s;
f(s);   // (const int&)
g(s);   // (short&)
h(s);   // ошибка - подошли hf(short&) --> f(const int&) и hg(short&) --> g(short&)


Если бы каждое из семейств состояло из одной функции, то мы могли бы попробовать извлечь её сигнатуру (boost/function_traits умеет, можем наколхозить руками)
Но для полиморфных функций — шаблонов и перегрузок — этот номер, очевидно, не пройдёт.

Если бы мы могли измерить вес подстановки, как это делает компилятор (точное попадание, cv, decay, шаблон, приведение типа — ну вот это вот всё), то можно было бы как-то наколхозить приоритеты и руками диспетчеризовывать.
Можем ли?
Я такую технику, увы, не знаю. Если знаете, подскажите.
Перекуём баги на фичи!
Re[4]: перегрузки с приоритетами
От: Кодт Россия  
Дата: 30.06.23 13:38
Оценка:
Здравствуйте, kov_serg, Вы писали:

_>А теперь обратная задача выяснить кого вызовут и где оно объявлено


Такие задачи возникают, например, при написании пользовательских хендлеров сравнения и вывода в GTest, и даже при работе с std::iostream.

Но, конечно, подход языка С++ и его компиляторов — оптимистичный. "Если юзер написал фиг знает что, — то, наверное, ему это было надо: держи well-formed (или ill-formed, no diagnostics required) программу, кушай не обляпайся"
Перекуём баги на фичи!
Re[2]: перегрузки с приоритетами
От: vopl Россия  
Дата: 30.06.23 15:12
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Если бы мы могли измерить вес подстановки, как это делает компилятор (точное попадание, cv, decay, шаблон, приведение типа — ну вот это вот всё), то можно было бы как-то наколхозить приоритеты и руками диспетчеризовывать.

К>Можем ли?

А там нет "веса".. Если вкратце/поверхностно — рассматривается конвертация одного аргумента->параметр, каждая конвертация относительно другой меряется как "лучшая/худшая", далее все эти конвертации сбиваются в кучу чтобы выйти на функцию в целом, функции так же выстраиваются в отношение, но немного другое -"лучшая/нелучшая". Далее на основании этого отношения уже выбирается перегрузка. Неоднозначность возникает когда наверху образуется пара "нелучших".. Тут детали https://timsong-cpp.github.io/cppwp/over.match.best
Re[2]: перегрузки с приоритетами
От: kov_serg Россия  
Дата: 30.06.23 15:18
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Ещё одна концептуальная беда — это невозможность (?) вытащить сигнатуру изнутри наружу.


А что мешает вместо dispatch делать return_function, а у неё брать сигнатуру ?
Re[3]: перегрузки с приоритетами
От: Кодт Россия  
Дата: 30.06.23 16:05
Оценка:
Здравствуйте, vopl, Вы писали:

К>>Если бы мы могли измерить вес подстановки, как это делает компилятор (точное попадание, cv, decay, шаблон, приведение типа — ну вот это вот всё), то можно было бы как-то наколхозить приоритеты и руками диспетчеризовывать.

К>>Можем ли?

V>А там нет "веса".. Если вкратце/поверхностно — рассматривается конвертация одного аргумента->параметр, каждая конвертация относительно другой меряется как "лучшая/худшая", далее все эти конвертации сбиваются в кучу чтобы выйти на функцию в целом, функции так же выстраиваются в отношение, но немного другое -"лучшая/нелучшая". Далее на основании этого отношения уже выбирается перегрузка. Неоднозначность возникает когда наверху образуется пара "нелучших".. Тут детали https://timsong-cpp.github.io/cppwp/over.match.best


Вот эти лучшая-нелучшая по каждому аргументу независимо и образуют вес ICS_i (13 уровней — они там в параграфах 2.1 — 2.13 перечислены, "or, if not that").

Над двумя векторами весов ICS задаётся частичный порядок: если все элементы одного лучше-или равны всех элементов другого, и хотя бы один строго лучше — весь вектор считается строго лучше — выбираем эту перегрузку.
Перекуём баги на фичи!
Re[3]: перегрузки с приоритетами
От: Кодт Россия  
Дата: 30.06.23 16:09
Оценка:
Здравствуйте, kov_serg, Вы писали:

_>А что мешает вместо dispatch делать return_function, а у неё брать сигнатуру ?


То, что семейство функций — не первоклассный объект. Как ты из hf вернёшь f?

Даже если сделать первоклассный объект с семейством operator(), — ну, будет всё то же самое. Будет семейство сигнатур этого оператора.
Перекуём баги на фичи!
Re[4]: перегрузки с приоритетами
От: kov_serg Россия  
Дата: 30.06.23 17:53
Оценка:
Здравствуйте, Кодт, Вы писали:

_>>А что мешает вместо dispatch делать return_function, а у неё брать сигнатуру ?

К>То, что семейство функций — не первоклассный объект. Как ты из hf вернёшь f?
А что мешает вместо line возвращать функцию или её номер.

enum {
    fun0_auto,
    fun5_int,
    fun5_const_char_ptr,
    fun10_ref_int,
    fun10,
    fun20_short,
    fun20_short_ref,
    fun20_long_ref,
    fun30_int_int
};

int fun(prio<0>, auto&&...) { return fun0_auto; }

int fun(prio<5>, int) { return fun5_int; }
int fun(prio<5>, const char*) { return fun5_const_char_ptr; }

int fun(prio<10>, int&) { return fun10_ref_int; }
int fun(prio<10>) { return fun10; }  // перегрузка с другим количеством аргументов

//int fun(prio<20>, short) { return fun20_short; }
int fun(prio<20>, short&) { return fun20_short_ref; }
int fun(prio<20>, long&) { return fun20_long_ref; }

int fun(prio<30>, int, int) { return fun30_int_int; }
Отредактировано 30.06.2023 17:59 kov_serg . Предыдущая версия .
Re[5]: перегрузки с приоритетами
От: Кодт Россия  
Дата: 30.06.23 20:56
Оценка:
Здравствуйте, kov_serg, Вы писали:

_>>>А что мешает вместо dispatch делать return_function, а у неё брать сигнатуру ?

К>>То, что семейство функций — не первоклассный объект. Как ты из hf вернёшь f?
_>А что мешает вместо line возвращать функцию или её номер.

__LINE__ — это чисто для демонстрации, просто чтоб видеть, что где произошло.
Можно возвращать __PRETTY_FUNCTION__ например.

Но дело в другом. Вернуть уникальный идентификатор функции несложно, если функция однозначно была выбрана как наилучшая.
Сложно, когда на ровном месте возникла ошибка неоднозначности.

Хотя!
Если мы договоримся, что наши функции возвращают не только собственно результат, но и информацию о себе самих.
Примерно так
template<class R, class... Args>
struct augmented_result {
  R value;
};


auto f(short x) -> augmented_result<int, short> { return {123}; }

auto f(auto&& x) -> augmented_result<int, decltype(x)> { return {123}; }

auto f(auto&&... xs) -> augmented_result<int, decltype(xs)...> { return {123}; }

auto f(auto x) {
  if constexpr(...) return augmented_result<int, decltype(x)>{123};
  else              return augmented_result<void, decltype(x)>{};
}

то потом банальное decltype(f(x)) нам даёт информацию о том, какие типы аргументов подошедшая перегрузка действительно ожидает.
И потом мы можем вручную воспроизвести логику выбора наилучшей перегрузки на основе сопоставления формальных и фактических типов.

Но писанины, конечно, при этом добавляется.
Перекуём баги на фичи!
Re[4]: перегрузки с приоритетами
От: vopl Россия  
Дата: 01.07.23 09:00
Оценка: +1
Здравствуйте, Кодт, Вы писали:

К>Здравствуйте, vopl, Вы писали:


К>>>Если бы мы могли измерить вес подстановки, как это делает компилятор (точное попадание, cv, decay, шаблон, приведение типа — ну вот это вот всё), то можно было бы как-то наколхозить приоритеты и руками диспетчеризовывать.

К>>>Можем ли?

V>>А там нет "веса".. Если вкратце/поверхностно — рассматривается конвертация одного аргумента->параметр, каждая конвертация относительно другой меряется как "лучшая/худшая", далее все эти конвертации сбиваются в кучу чтобы выйти на функцию в целом, функции так же выстраиваются в отношение, но немного другое -"лучшая/нелучшая". Далее на основании этого отношения уже выбирается перегрузка. Неоднозначность возникает когда наверху образуется пара "нелучших".. Тут детали https://timsong-cpp.github.io/cppwp/over.match.best


К>Вот эти лучшая-нелучшая по каждому аргументу независимо и образуют вес ICS_i (13 уровней — они там в параграфах 2.1 — 2.13 перечислены, "or, if not that").


К>Над двумя векторами весов ICS задаётся частичный порядок: если все элементы одного лучше-или равны всех элементов другого, и хотя бы один строго лучше — весь вектор считается строго лучше — выбираем эту перегрузку.


Ну, немножко наоборот, сначала перегрузки оцениваются во векторам ICS (https://timsong-cpp.github.io/cppwp/over.match.best#general-1.sentence-2) и если в результате не находится "лучшей" — применяются те 13 пунктов (https://timsong-cpp.github.io/cppwp/over.match.best#general-2). А в них уже некоторое количество всяких специальных случаев выбора, например на основе возвращаемого типа (в случае user defined conversion, reference binding), на основании шаблонности во всяких разных позах и прочие.
Re[2]: перегрузки с приоритетами
От: vopl Россия  
Дата: 01.07.23 14:39
Оценка: 68 (1)
Здравствуйте, Кодт, Вы писали:


К>Что мне в ней не нравится концептуально:

К>1) если есть несколько подходящих перегрузок с разными приоритетами, и у той, что подходит лучше (меньшее количество преобразований типа) приоритет ниже, — до неё дело не дойдёт
К>2) если есть несколько подходящих перегрузок с одним приоритетом, — ошибка неоднозначности трактуется как неудача подстановки
К>3) наличие/отсутствие ограничений (concept, requires) не пробрасывается из семейства перегрузок в полиморфную лямбду и может создавать неоднозначность (и см. пункт 2)

первые два момента получилось закрыть примерно так
#include <iostream>

template <std::size_t v> struct prio : prio<v-1> {};
template <> struct prio<0> {};

void f(prio<0>, short) {std::cout << "short" << std::endl;}
void f(prio<1>, int)   {std::cout << "int"   << std::endl;}
void f(prio<2>, long)  {std::cout << "long"  << std::endl;}

template<class... Args>
constexpr bool invocableWith = requires(Args... args) {{f(args...)};};

template <std::size_t pv=10, class... Args>
requires invocableWith<prio<pv>, Args...>
void dispatch(Args... args)
{
    f(prio<pv>{}, args...);
}

template <std::size_t pv=10, class... Args>
requires (pv>0 && !invocableWith<prio<pv>, Args...>)
void dispatch(Args... args)
{
    dispatch<pv-1>(args...);
}

template <std::size_t pv, class... Args>
requires (pv==0 && !invocableWith<prio<pv>, Args...>)
void dispatch(Args... args) = delete;

int main()
{
    // выбор по второму аргументу
    dispatch(short{});
    dispatch(long{});
    dispatch(int{});

    // по второму аргументу коллизия, выбор по приоритету
    dispatch(unsigned{});
    dispatch(float{});

    // нет вариантов
    struct Bad{};
    //dispatch(Bad{});

    return 0;
}


https://gcc.godbolt.org/z/bTqzoWPWG
Re[3]: перегрузки с приоритетами
От: vopl Россия  
Дата: 01.07.23 15:14
Оценка:
адаптировал исходный пример
#include <cstddef>
#include <utility>

////////////////////////////////////////////////////////////////////////////
template <std::size_t v> struct prio : prio<v-1> {};
template <> struct prio<0> {};



////////////////////////////////////////////////////////////////////////////
int fun(prio<0>, auto&&...) { return __LINE__; }

int fun(prio<5>, int) { return __LINE__; }
int fun(prio<5>, const char*) { return __LINE__; }

int fun(prio<10>, int&) { return __LINE__; }
int fun(prio<10>) { return __LINE__; }  // перегрузка с другим количеством аргументов

int fun(prio<19>, short) { return __LINE__; }
int fun(prio<20>, short&) { return __LINE__; }
int fun(prio<20>, long&) { return __LINE__; }

int fun(prio<30>, int, int) { return __LINE__; }



////////////////////////////////////////////////////////////////////////////
template<class... Args>
constexpr bool invocableWith = requires(Args&&... args) {{fun(std::forward<Args>(args)...)};};

template <std::size_t pv=100, class... Args>
requires invocableWith<prio<pv>, Args...>
decltype(auto) do_fun(Args&&... args)
{
    return fun(prio<pv>{}, std::forward<Args>(args)...);
}

template <std::size_t pv=100, class... Args>
requires (pv>0 && !invocableWith<prio<pv>, Args...>)
decltype(auto) do_fun(Args&&... args)
{
    return do_fun<pv-1>(std::forward<Args>(args)...);
}

template <std::size_t pv, class... Args>
requires (pv==0 && !invocableWith<prio<pv>, Args...>)
void do_fun(Args&&... args) = delete;



////////////////////////////////////////////////////////////////////////////
#include <iostream>
#define TEST(expr)  std::cout << #expr << " = " << (expr) << std::endl;
int main() {
    TEST(do_fun());
    TEST(do_fun(1));
    TEST(do_fun(""));
    int x;
    TEST(do_fun(x));
    long y;
    TEST(do_fun(y));
    float z;
    TEST(do_fun(z));
    TEST(do_fun(1, 2));
    short t;
    TEST(do_fun(t));
}

https://gcc.godbolt.org/z/cPW41Md4a

разница получиласть только в варианте "float z", в оригинале он проводится на "int", а тут на "auto&&". Но эта разница скорее методологическая, потому что в оригинале сначала отрабатывает приоритет и только потом подстановка аргумента, а тут наоборот.
Re[3]: перегрузки с приоритетами
От: Кодт Россия  
Дата: 04.07.23 15:45
Оценка:
Здравствуйте, vopl, Вы писали:

V>первые два момента получилось закрыть примерно так

V>[ccode]
V>#include <iostream>

V>template <std::size_t v> struct prio : prio<v-1> {};

V>template <> struct prio<0> {};
[/c]
думал об этом, но видимо, какие-то первые неудачные эксперименты меня столкнули с этой дороги.

Кстати, надо проверить, насколько хорошо это будет работать, если содержательные аргументы тоже образуют иерархию. И если там более одного аргумента.
Возможно, именно это меня и столкнуло, уже не помню.

Небольшое допиливание, чтобы не делать этажерку вызовов на всю глубину.
https://gcc.godbolt.org/z/6MY9o4hGh
Перекуём баги на фичи!
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.