Что мне в ней не нравится концептуально:
1) если есть несколько подходящих перегрузок с разными приоритетами, и у той, что подходит лучше (меньшее количество преобразований типа) приоритет ниже, — до неё дело не дойдёт
2) если есть несколько подходящих перегрузок с одним приоритетом, — ошибка неоднозначности трактуется как неудача подстановки
3) наличие/отсутствие ограничений (concept, requires) не пробрасывается из семейства перегрузок в полиморфную лямбду и может создавать неоднозначность (и см. пункт 2)
Что мне не нравится технически:
Выбор приоритета можно делать разными способами.
// тупо рекурсией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 повлекло за собой необходимость всегда возвращать значение приоритета. А это — потребовало ввести значение "ошибочный приоритет".
Как сделать красиво и чтобы функция из тотальной превратилась в частичную, у меня фантазия кончилась. Некрасиво сделать можно, но там будет много чего вытащено в сигнатуры, мясом наружу.
круто. Только ещё бы понять зачем
Вот в некоторых языках вовсе запрещают перегрузки — наверное боятся чего-то (или лучше сказать кого-то). А писать код, который мало того что рассчитывает на правила перегрузки, но ещё и смотрит приоритет... Это ж один напишет код, отладит и уволится. За ним придёт новичок, будет фиксить баг. Посмотрит: приоритеты какие-то. Дай-ка поменяю, авось заработает.
Здравствуйте, sergii.p, Вы писали:
SP>круто. Только ещё бы понять зачем
Академический интерес.
Задумался о природе и о способах практического разрешения неоднозначностей (т.е. — как хочет программист, а не как навязывает компилятор из коробки), причём это касается разных языков, не только плюсов.
Как управлять вселенной, не привлекая внимания санитаров влиять на семантику языка, не переписывая компилятор.
На том же питоне, например, можно было бы строить полиморфные функции с нетривиальной диспетчеризацией, просто оббежав всю коллекцию (или, например, граф суперклассов), прочитать аннотации и реализовать абсолютно любую логику.
Хорошо, когда рефлексия есть. А ещё лучше, когда есть запуск пользовательского кода в компиляторе — макросы, вот это вот всё.
А когда нет?
У С++ нет рефлексии, но есть очень затейливые возможности по метапрограммированию.
Тем и интересна эта задача.
Проиллюстрировать теорию на практике.
SP>Вот в некоторых языках вовсе запрещают перегрузки — наверное боятся чего-то (или лучше сказать кого-то).
Или руки не из того места растут.
Можно вспомнить несчастный ML, с его хиндли-милнером, прибитым гвоздями, — и как потом во всех его потомках — что в OcaML, что в F#, разными способами прикручивали ад-хок полиморфизмы.
Даже арифметика там не перегружена, (1 + 2) против (1.0 +. 2.0)
SP>А писать код, который мало того что рассчитывает на правила перегрузки, но ещё и смотрит приоритет...
И мы знаем такие примеры.
В парсерах ряда языков можно и нужно задавать приоритеты (пользовательских) инфиксных операторов.
А с другой стороны, в некоторых языках (семейство APL, семейство SmallTalk) все операторы имеют один приоритет, да ещё и ассоциативность может удивить.
SP>Это ж один напишет код, отладит и уволится. За ним придёт новичок, будет фиксить баг. Посмотрит: приоритеты какие-то. Дай-ка поменяю, авось заработает.
Программирование методом научного тыка — это увлекательно.
Для опытного новичка даже приоритеты не нужны, он просто натыкает поправочных констант плюс-минус-1 по всему коду и авось заработает.
Здравствуйте, Кодт, Вы писали:
К>Академический интерес. К>Программирование методом научного тыка — это увлекательно.
А теперь обратная задача выяснить кого вызовут и где оно объявлено
Эти два шаблона говорят: "если аргумент x можно передать в f (или g, соответственно), — то давайте передадим".
Разумеется, каждый из шаблонов выбирает наилучшую подходящую перегрузку. Но наружу них обоих торчит всего-навсего
Если бы каждое из семейств состояло из одной функции, то мы могли бы попробовать извлечь её сигнатуру (boost/function_traits умеет, можем наколхозить руками)
Но для полиморфных функций — шаблонов и перегрузок — этот номер, очевидно, не пройдёт.
Если бы мы могли измерить вес подстановки, как это делает компилятор (точное попадание, cv, decay, шаблон, приведение типа — ну вот это вот всё), то можно было бы как-то наколхозить приоритеты и руками диспетчеризовывать.
Можем ли?
Я такую технику, увы, не знаю. Если знаете, подскажите.
Здравствуйте, kov_serg, Вы писали:
_>А теперь обратная задача выяснить кого вызовут и где оно объявлено
Такие задачи возникают, например, при написании пользовательских хендлеров сравнения и вывода в GTest, и даже при работе с std::iostream.
Но, конечно, подход языка С++ и его компиляторов — оптимистичный. "Если юзер написал фиг знает что, — то, наверное, ему это было надо: держи well-formed (или ill-formed, no diagnostics required) программу, кушай не обляпайся"
Здравствуйте, Кодт, Вы писали:
К>Если бы мы могли измерить вес подстановки, как это делает компилятор (точное попадание, cv, decay, шаблон, приведение типа — ну вот это вот всё), то можно было бы как-то наколхозить приоритеты и руками диспетчеризовывать. К>Можем ли?
А там нет "веса".. Если вкратце/поверхностно — рассматривается конвертация одного аргумента->параметр, каждая конвертация относительно другой меряется как "лучшая/худшая", далее все эти конвертации сбиваются в кучу чтобы выйти на функцию в целом, функции так же выстраиваются в отношение, но немного другое -"лучшая/нелучшая". Далее на основании этого отношения уже выбирается перегрузка. Неоднозначность возникает когда наверху образуется пара "нелучших".. Тут детали https://timsong-cpp.github.io/cppwp/over.match.best
Здравствуйте, 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 задаётся частичный порядок: если все элементы одного лучше-или равны всех элементов другого, и хотя бы один строго лучше — весь вектор считается строго лучше — выбираем эту перегрузку.
Здравствуйте, Кодт, Вы писали:
_>>А что мешает вместо 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; }
Здравствуйте, 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)) нам даёт информацию о том, какие типы аргументов подошедшая перегрузка действительно ожидает.
И потом мы можем вручную воспроизвести логику выбора наилучшей перегрузки на основе сопоставления формальных и фактических типов.
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, 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 задаётся частичный порядок: если все элементы одного лучше-или равны всех элементов другого, и хотя бы один строго лучше — весь вектор считается строго лучше — выбираем эту перегрузку.
К>Что мне в ней не нравится концептуально: К>1) если есть несколько подходящих перегрузок с разными приоритетами, и у той, что подходит лучше (меньшее количество преобразований типа) приоритет ниже, — до неё дело не дойдёт К>2) если есть несколько подходящих перегрузок с одним приоритетом, — ошибка неоднозначности трактуется как неудача подстановки К>3) наличие/отсутствие ограничений (concept, requires) не пробрасывается из семейства перегрузок в полиморфную лямбду и может создавать неоднозначность (и см. пункт 2)
первые два момента получилось закрыть примерно так
разница получиласть только в варианте "float z", в оригинале он проводится на "int", а тут на "auto&&". Но эта разница скорее методологическая, потому что в оригинале сначала отрабатывает приоритет и только потом подстановка аргумента, а тут наоборот.
Здравствуйте, vopl, Вы писали:
V>первые два момента получилось закрыть примерно так V>[ccode] V>#include <iostream>
V>template <std::size_t v> struct prio : prio<v-1> {}; V>template <> struct prio<0> {};
[/c]
думал об этом, но видимо, какие-то первые неудачные эксперименты меня столкнули с этой дороги.
Кстати, надо проверить, насколько хорошо это будет работать, если содержательные аргументы тоже образуют иерархию. И если там более одного аргумента.
Возможно, именно это меня и столкнуло, уже не помню.