Обычно у меня есть EnumType::invalidValue, я его передаю как defVal, и если на выходе получил EnumType::invalidValue, то кидаю исключение.
Поиск реализации идёт через ADL по дефолтному значению, и всё в общем-то работает.
Она сама умеет кидать исключение, но её имя генерится на макросах и зависит от имени enum'а.
Теперь я хочу сделать шаблонную функцию enum_deserialize, чтобы тип enum'а она получала как параметр шаблона.
Пока я сделал, основываясь на том, что EnumType::invalidValue у меня всегда -1, и я использую первую версию, передаю туда инвалида (-1), и на выходе с этим инвалидом сравниваю, и кидаю исключение, если что-то пошло не так.
Но это завязано на мои соглашения, что EnumType::invalidValue у меня всегда -1. С enum'ами, константы для которых могут зависеть от внешних спек, такое может не прокатить.
Наивная идея была сделать общий шаблон enum_deserialize_throwable, и генерить специализации для него. Но потом я вспомнил, что все сгенерённые функции попадают в то же пространство имён, что и сам enum, и это могут быть любые NS, и точно не то NS, в котором объявлен шаблон, который я хотел специализировать.
Здравствуйте, Marty, Вы писали:
M>Но это завязано на мои соглашения, что EnumType::invalidValue у меня всегда -1. С enum'ами, константы для которых могут зависеть от внешних спек, такое может не прокатить.
M>Наивная идея была сделать общий шаблон enum_deserialize_throwable, и генерить специализации для него. Но потом я вспомнил, что все сгенерённые функции попадают в то же пространство имён, что и сам enum, и это могут быть любые NS, и точно не то NS, в котором объявлен шаблон, который я хотел специализировать.
M>Как быть?
Честно признаюсь, не уверен, что полностью правильно понял все сценарии, но попробую предложить решение, а вдруг попаду. Ты можешь для каждого энума определить функцию получения дефолтного значения. Разумеется, эта функция должна быть определена в том же пространстве имен, что и сам энум — чтобы она могла быть найденной через ADL. Один лишь нюанс: для того, чтобы ADL правильно работал, у этой функции должен быть один формальный параметр, имеющий тип этого энума. При вызове этих функций можно будет передавать фактический параметр, сконструированный по дефолту. Еще один замечательный момент: эти функции, получающие дефолтный параметр могут быть как шаблонными, так и обычными, нешаблонными. Схематично так:
Можно еще заморочиться и создать универсальную шаблонную обертку вообще для всех-всех типов энумов, которая будет создавать дефолтный экземпляр энума нужного типа и передавать его в "рабочу лошадку" — акксесор:
Но это уже для совсем уж больших любителей синтаксического сахара, без этого вполне можно жить, я считаю. И надо понимать, что эта обертка не сможет находиться по ADL, в отличие от реальных аксессоров, определенных в пространствах имен энумов, и объявление этой обертки всегда должно быть видно в точке ее использования.
--
Не можешь достичь желаемого — пожелай достигнутого.
Здравствуйте, Marty, Вы писали:
M>Здравствуйте!
M>Наивная идея была сделать общий шаблон enum_deserialize_throwable, и генерить специализации для него. Но потом я вспомнил, что все сгенерённые функции попадают в то же пространство имён, что и сам enum, и это могут быть любые NS, и точно не то NS, в котором объявлен шаблон, который я хотел специализировать.
Но ты же сам пишешь:
M>Она на внешнем генераторе
Это означает, что самое сложное препятствие уже устранено, и в генераторе достаточно буквально пару строк в коде поменять, чтобы закрыть namespace чуть раньше, чтобы специализация оказалось в том единственном namespace, к котором существует декларация шаблона
template <class E>
E enum_deserialize<E>(std::string_view str);
Это может быть глобальный namespace или лучше какой-нибудь условно my_enum_serialization, но главное, что этот ns будет одним фиксированными и не зависящим от типа перечисления.
M>Обычно у меня есть EnumType::invalidValue, я его передаю как defVal, и если на выходе получил EnumType::invalidValue, то кидаю исключение.
Это, конечно, не очень хороший подход. Лучше сделать шаблон вида:
А код с исключением сделать универсальным вне генератора:
template <class E>
E enum_deserialize_throwable(std::string_view str) {
std::optional r = try_enum_deserialize<E>(str);
return r.has_value() ? *r : throw std::domain_error(std::format("Unknown value in enum {}: {:?}", typeid(E).name(), str));
}
template <class E>
E enum_deserialize_or_default(std::string_view str, E default_value) {
return try_enum_deserialize<E>(str).value_or(default_value);
}
И тебе вообще оказывается не нужно ADL, так как во всех функциях уже известно что за конкретный тип за E скрывается.
M> в результате его работы в том же пространстве имён, в котором объявлен enum, появляется функция: M>
и она продолжает работать.
Но я бы посоветовал вообще такую функцию не генерировать в том же пространстве имён, что и перечисление. А сразу вызывать шаблонную enum_deserialize_or_default, а тип компилятор из defVal сам выведет и найдёт нужную специализацию. Тут поиск по ADL никакой дополнительной пользы не несёт.
M>Она сама умеет кидать исключение, но её имя генерится на макросах и зависит от имени enum'а.
Тут ещё бы сильнее посоветовал не генерировать такую функцию вообще, а сразу использовать шаблоны.
Потому что в месте вызова ты раньше писал
EnumType e = enum_deserialize_EnumType("foo");
а теперь будешь писать
EnumType e = ::enum_deserialize<EnumType>("foo");
Если не нужно вызывать эти функции из C (где нет шаблонов и классов), то нет хороших причин предпочитать первый способ второму. Но зато второй способ снимает кучу проблем с определением какой-же там тип, в каком пространстве имём он расположен, и какой include нужно подключить, чтобы получить эту декларацию.
Единственное, что нужно сделать — это добавить include c декларацией шаблона, чтобы компилятор понимал, что есть такой шаблон. При этом его специализации не обязательно даже показывать, так как их связывание можно аж до link-time отложить.
Кстати, сериализация перечислений — это как раз хороший пример, где можно не делать код преобразований в строку и обратно в inline функциях, а вынести реализацию из заголовочных файлов в cpp, — шаблоны этому не мешают. И такой вынос даже на скорости компиляции проекта может положительно сказаться — компилятору не нужно будет портянку автосгенерированного кода в include каждый раз разбирать — если include подключается больше чем в одно место в программе, то уже выигрыш появляется.
Здравствуйте, watchmaker, Вы писали:
M>>Наивная идея была сделать общий шаблон enum_deserialize_throwable, и генерить специализации для него. Но потом я вспомнил, что все сгенерённые функции попадают в то же пространство имён, что и сам enum, и это могут быть любые NS, и точно не то NS, в котором объявлен шаблон, который я хотел специализировать.
W>Но ты же сам пишешь:
M>>Она на внешнем генераторе
W>Это означает, что самое сложное препятствие уже устранено, и в генераторе достаточно буквально пару строк в коде поменять, чтобы закрыть namespace чуть раньше, чтобы специализация оказалось в том единственном namespace, к котором существует декларация шаблона
Остальное потом проосмыслю, но тут — сразу нет
Генератор берёт пачку определений enum на входе и открытие/закрытие NS вставляет в начало и конец сгенерённого файла. Внутри он не умеет прыгать по разным NS туда-сюда, это кучу кода генератора надо переделывать. Если enum'ы требуются в разные NS — это отдельные запуски генератора со своими отдельными определениями enum'ов и отдельным заданием namespace'ов
Грубо говоря, у генератора есть опция --namespace=my/name/space
В коде генератора есть что-то типа
{
auto nsGenGuard = NsGuard.parseOption(cmdLine, "namespace", std::cout);
// Тут генерится
// namespace my{
// namespace name{
// namespace space{
// Тут фигачим генерацию
// Тут скоп закончился, и выводится
// } // namespace space
// } // namespace name
// } // namespace my
}
Здравствуйте, rg45, Вы писали:
R>Честно признаюсь, не уверен, что полностью правильно понял все сценарии, но попробую предложить решение, а вдруг попаду.
Раскурю твой ответ завтра
Не стоило цитировать всю мою простыню, я до твоего ответа с трудом добрался
M>Генератор берёт пачку определений enum на входе и открытие/закрытие NS вставляет в начало и конец сгенерённого файла. Внутри он не умеет прыгать по разным NS туда-сюда, это кучу кода генератора надо переделывать.
А ему и не нужно это уметь. В твоём enum-gen достаточно вызвать enum_generate_serialize второй раз только с флагом "сделай_специлизации_для_io" после ns-guard (а из первого вызова под ns-guard эти флаги убрать).
И при этом флаги у тебя уже есть — в остальном там реально в самом генераторе переделывать-то строчек 5
M>Генератор сильно переписать не вариант, более реальный вариант поправить что-то в макросной консерватории, которую использует генератор
Здравствуйте, watchmaker, Вы писали:
M>>Генератор берёт пачку определений enum на входе и открытие/закрытие NS вставляет в начало и конец сгенерённого файла. Внутри он не умеет прыгать по разным NS туда-сюда, это кучу кода генератора надо переделывать.
W>А ему и не нужно это уметь. В твоём enum-gen достаточно вызвать enum_generate_serialize второй раз только с флагом "сделай_специлизации_для_io" после ns-guard (а из первого вызова под ns-guard эти флаги убрать). W>И при этом флаги у тебя уже есть — в остальном там реально в самом генераторе переделывать-то строчек 5
M>>Генератор сильно переписать не вариант, более реальный вариант поправить что-то в макросной консерватории, которую использует генератор
W>Дело твоё, но ты выбираешь очень сложный путь.
Поправить макросы в сорцах мне пока всё ещё кажется более простым путём, чем допиливать генератор и деплоить его всем нуждающимся
M>Есть у меня сериализация/десериализация enum'ов.
M>Она на внешнем генераторе, в результате его работы в том же пространстве имён, в котором объявлен enum, появляется функция:
Здравствуйте, K13, Вы писали:
M>>Есть у меня сериализация/десериализация enum'ов.
M>>Она на внешнем генераторе, в результате его работы в том же пространстве имён, в котором объявлен enum, появляется функция:
K13>Зачем изобретать велосипед?
K13>https://github.com/Neargye/magic_enum
Ограничения смотрели ?
Enum value must be in range [MAGIC_ENUM_RANGE_MIN, MAGIC_ENUM_RANGE_MAX].
By default MAGIC_ENUM_RANGE_MIN = -128, MAGIC_ENUM_RANGE_MAX = 128.
В некоторых случаях это не проблема, но не для общего решения любого перечисления.
Здравствуйте, _NN_, Вы писали:
K13>>Зачем изобретать велосипед?
K13>>https://github.com/Neargye/magic_enum
_NN>Ограничения смотрели ?
_NN>Enum value must be in range [MAGIC_ENUM_RANGE_MIN, MAGIC_ENUM_RANGE_MAX].
_NN>By default MAGIC_ENUM_RANGE_MIN = -128, MAGIC_ENUM_RANGE_MAX = 128.
_NN>В некоторых случаях это не проблема, но не для общего решения любого перечисления.
Там есть специализация для конкретного енума, если надо:
If need another range for specific enum type, add specialization enum_range for necessary enum type. Specialization of enum_range must be injected in namespace magic_enum::customize.
#include <magic_enum.hpp>
enum class number { one = 100, two = 200, three = 300 };
template <>
struct magic_enum::customize::enum_range<number> {
static constexpr int min = 100;
static constexpr int max = 300;
// (max - min) must be less than UINT16_MAX.
};
единственное реальное ограничение -- диапазон енумов должен влезать в UINT16_MAX
Здравствуйте, K13, Вы писали:
K13>единственное реальное ограничение
Нужно быть честными: это всё же далеко не единственное серьёзное ограничение.
Даже Marty в первом сообщении показывает своё перечисление, в котором есть алиасы, которые в magic_enum не будут работать никак.
Да и magic_enum — это в первую очередь интересное техническое демо как хакнуть компилятор (как и другие magic_* демки), и лишь во вторую очередь что-то пригодное к использованию
K13> диапазон енумов должен влезать в UINT16_MAX
Это даже на современном железе будет давать плюс пару десятков секунд компиляции для каждого перечисления для каждой затронутой единицы трансляции, в которую транзитивно попадёт что-то, использующее библиотеку. И результат не кешируется между запусками: время придётся тратить при каждой компиляции даже если в перечислении ничего не поменялось, а поменялся какой-то другой код.
И в readme не просто так сразу упомянуто, что библиотека будет вызывать срабатывания watch-dog'ов во всех компиляторах, которые слезят за зависаниями компилятора.
Лимиты, конечно, можно увеличить. Но разрабатываться всё равно станет тяжело из-за длительной компиляции: ждать минуту прежде чем ide или компилятор подскажет, что, например, в имени переменной или в имени класса опечатка, — это уже кажется диким.
Понятно, что если в приложении всего пара крошечных перечислений, то библиотеку можно аккуратно использовать. Но раскручивать лимиты или добавлять в большой проект — скорее нет.
Главное преимущество библиотеки — для неё не нужно что-то делать в системе сборки. С чем в С++ есть большие исторические проблемы. И тут можно понять некоторых людей, которые готовы пожертвовать временем каждой компиляции, чтобы не разбираться как это сделать в cmake, meson, bazel или в чём-то другом.
Но если кодогенератор уже написан и используется, то переход на magic_enum — это почти со всех сторон пессимизация: меньше возможностей, замедляет компиляцию, хуже отслеживает необходимость пересборки, и имеет ограничения, которые никак не обойти, если в них упрёшся.