ADL не ADL? Или как?
От: Marty Пират https://www.youtube.com/channel/UChp5PpQ6T4-93HbNF-8vSYg
Дата: 07.01.24 19:42
Оценка:
Здравствуйте!

Есть у меня сериализация/десериализация enum'ов.

Она на внешнем генераторе, в результате его работы в том же пространстве имён, в котором объявлен enum, появляется функция:
inline
EnumType enum_deserialize(const std::string &str, EnumType defVal);


Обычно у меня есть EnumType::invalidValue, я его передаю как defVal, и если на выходе получил EnumType::invalidValue, то кидаю исключение.
Поиск реализации идёт через ADL по дефолтному значению, и всё в общем-то работает.

Есть ещё функция
inline
EnumType enum_deserialize_##EnumType(const std::string &str);


Она сама умеет кидать исключение, но её имя генерится на макросах и зависит от имени enum'а.

Теперь я хочу сделать шаблонную функцию enum_deserialize, чтобы тип enum'а она получала как параметр шаблона.
Пока я сделал, основываясь на том, что EnumType::invalidValue у меня всегда -1, и я использую первую версию, передаю туда инвалида (-1), и на выходе с этим инвалидом сравниваю, и кидаю исключение, если что-то пошло не так.

Но это завязано на мои соглашения, что EnumType::invalidValue у меня всегда -1. С enum'ами, константы для которых могут зависеть от внешних спек, такое может не прокатить.

Наивная идея была сделать общий шаблон enum_deserialize_throwable, и генерить специализации для него. Но потом я вспомнил, что все сгенерённые функции попадают в то же пространство имён, что и сам enum, и это могут быть любые NS, и точно не то NS, в котором объявлен шаблон, который я хотел специализировать.

Как быть?


ЗЫ Генератор генерит кот через эти матросики — https://github.com/al-martyn1/marty_cpp/blob/main/marty_enum.h
Пример сгенерённого кода — https://github.com/al-martyn1/marty_cpp/blob/main/enums.h
Маньяк Робокряк колесит по городу
Отредактировано 07.01.2024 19:50 Marty . Предыдущая версия .
Re: ADL не ADL? Или как?
От: rg45 СССР  
Дата: 07.01.24 20:42
Оценка:
Здравствуйте, Marty, Вы писали:

M>Но это завязано на мои соглашения, что EnumType::invalidValue у меня всегда -1. С enum'ами, константы для которых могут зависеть от внешних спек, такое может не прокатить.


M>Наивная идея была сделать общий шаблон enum_deserialize_throwable, и генерить специализации для него. Но потом я вспомнил, что все сгенерённые функции попадают в то же пространство имён, что и сам enum, и это могут быть любые NS, и точно не то NS, в котором объявлен шаблон, который я хотел специализировать.


M>Как быть?


Честно признаюсь, не уверен, что полностью правильно понял все сценарии, но попробую предложить решение, а вдруг попаду. Ты можешь для каждого энума определить функцию получения дефолтного значения. Разумеется, эта функция должна быть определена в том же пространстве имен, что и сам энум — чтобы она могла быть найденной через ADL. Один лишь нюанс: для того, чтобы ADL правильно работал, у этой функции должен быть один формальный параметр, имеющий тип этого энума. При вызове этих функций можно будет передавать фактический параметр, сконструированный по дефолту. Еще один замечательный момент: эти функции, получающие дефолтный параметр могут быть как шаблонными, так и обычными, нешаблонными. Схематично так:

namespace ns1
{

enum E1
{
   invalidValue = -1,
   . . .
};

enum E2
{
   invalidValue = -1,
   . . .
};

enum E3
{
   None = -42,
   . . .
};

enum E4
{
   // No invalid value defined
   Good1 = 1,
   Good2 = 2,
   Good3 = 3
};

// Comon accessor
template <typename T>
constexpr T enum_default_value(T) { return T::invalidValue; }

// Special accessor for E3
constexpr E3 enum_default_value(E3) { return E3::None; }

// Special accessor for E4
constexpr E4 enum_default_value(E4) { return {}; } // Returns zero-initialized E4 object

} // namespace ns1

Ну и потом где-то дальше ты сможешь единообразно получать дефолтные значения для разных типов энумов:

namespace ns2
{

void usage_example()
{
  enum_default_value(ns1::E1{}); // returns E1::invalidValue
  enum_default_value(ns1::E2{}); // returns E2::invalidValue
  enum_default_value(ns1::E3{}); // returns E3::None
  enum_default_value(ns1::E4{}); // returns E4{}
}

} // namespace ns2



Можно еще заморочиться и создать универсальную шаблонную обертку вообще для всех-всех типов энумов, которая будет создавать дефолтный экземпляр энума нужного типа и передавать его в "рабочу лошадку" — акксесор:

namespace ns2
{

template <typename T>
constexpr enum_default_value() { return enum_default_value(T{}); }

void usage_example()
{
  enum_default_value<ns1::E1>(); // returns E1::invalidValue
  enum_default_value<ns1::E2>(); // returns E2::invalidValue
  enum_default_value<ns1::E3>(); // returns E3::None
  enum_default_value<ns1::E4>(); // returns E4{}
}
} // namespace ns2

Но это уже для совсем уж больших любителей синтаксического сахара, без этого вполне можно жить, я считаю. И надо понимать, что эта обертка не сможет находиться по ADL, в отличие от реальных аксессоров, определенных в пространствах имен энумов, и объявление этой обертки всегда должно быть видно в точке ее использования.
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 08.01.2024 10:41 rg45 . Предыдущая версия . Еще …
Отредактировано 07.01.2024 21:08 rg45 . Предыдущая версия .
Отредактировано 07.01.2024 21:01 rg45 . Предыдущая версия .
Отредактировано 07.01.2024 20:57 rg45 . Предыдущая версия .
Отредактировано 07.01.2024 20:55 rg45 . Предыдущая версия .
Отредактировано 07.01.2024 20:55 rg45 . Предыдущая версия .
Отредактировано 07.01.2024 20:54 rg45 . Предыдущая версия .
Отредактировано 07.01.2024 20:46 rg45 . Предыдущая версия .
Отредактировано 07.01.2024 20:44 rg45 . Предыдущая версия .
Re: ADL не ADL? Или как?
От: watchmaker  
Дата: 08.01.24 02:41
Оценка:
Здравствуйте, 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>
std::optional<E> try_enum_deserialize(std::string_view str);

И для него генерировать специализации.

А код с исключением сделать универсальным вне генератора:

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>
M>inline
M>EnumType enum_deserialize(const std::string &str, EnumType defVal);
M>

Если нужно для обратной совместимости, то для этой функции теперь генерируешь тело:
  return ::my_enum_serialization::enum_deserialize_or_default(str, defVal);
и она продолжает работать.
Но я бы посоветовал вообще такую функцию не генерировать в том же пространстве имён, что и перечисление. А сразу вызывать шаблонную enum_deserialize_or_default, а тип компилятор из defVal сам выведет и найдёт нужную специализацию. Тут поиск по ADL никакой дополнительной пользы не несёт.


M>Есть ещё функция

M>
M>inline
M>EnumType enum_deserialize_##EnumType(const std::string &str);
M>

M>Она сама умеет кидать исключение, но её имя генерится на макросах и зависит от имени enum'а.
Тут ещё бы сильнее посоветовал не генерировать такую функцию вообще, а сразу использовать шаблоны.

Потому что в месте вызова ты раньше писал
EnumType e = enum_deserialize_EnumType("foo");

а теперь будешь писать
EnumType e = ::enum_deserialize<EnumType>("foo");

Если не нужно вызывать эти функции из C (где нет шаблонов и классов), то нет хороших причин предпочитать первый способ второму. Но зато второй способ снимает кучу проблем с определением какой-же там тип, в каком пространстве имём он расположен, и какой include нужно подключить, чтобы получить эту декларацию.

Единственное, что нужно сделать — это добавить include c декларацией шаблона, чтобы компилятор понимал, что есть такой шаблон. При этом его специализации не обязательно даже показывать, так как их связывание можно аж до link-time отложить.
Кстати, сериализация перечислений — это как раз хороший пример, где можно не делать код преобразований в строку и обратно в inline функциях, а вынести реализацию из заголовочных файлов в cpp, — шаблоны этому не мешают. И такой вынос даже на скорости компиляции проекта может положительно сказаться — компилятору не нужно будет портянку автосгенерированного кода в include каждый раз разбирать — если include подключается больше чем в одно место в программе, то уже выигрыш появляется.
Re[2]: ADL не ADL? Или как?
От: Marty Пират https://www.youtube.com/channel/UChp5PpQ6T4-93HbNF-8vSYg
Дата: 08.01.24 02:59
Оценка:
Здравствуйте, 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

}



Я пример сгенеренного приводил — https://github.com/al-martyn1/marty_cpp/blob/main/enums.h

Там правда только нет вложенных NS

Генератор сильно переписать не вариант, более реальный вариант поправить что-то в макросной консерватории, которую использует генератор
Маньяк Робокряк колесит по городу
Отредактировано 08.01.2024 3:09 Marty . Предыдущая версия . Еще …
Отредактировано 08.01.2024 3:03 Marty . Предыдущая версия .
Re[2]: ADL не ADL? Или как?
От: Marty Пират https://www.youtube.com/channel/UChp5PpQ6T4-93HbNF-8vSYg
Дата: 08.01.24 03:45
Оценка:
Здравствуйте, rg45, Вы писали:

R>Честно признаюсь, не уверен, что полностью правильно понял все сценарии, но попробую предложить решение, а вдруг попаду.


Раскурю твой ответ завтра

Не стоило цитировать всю мою простыню, я до твоего ответа с трудом добрался
Маньяк Робокряк колесит по городу
Re[3]: ADL не ADL? Или как?
От: watchmaker  
Дата: 08.01.24 03:46
Оценка:
Здравствуйте, Marty, Вы писали:


M>Генератор берёт пачку определений enum на входе и открытие/закрытие NS вставляет в начало и конец сгенерённого файла. Внутри он не умеет прыгать по разным NS туда-сюда, это кучу кода генератора надо переделывать.


А ему и не нужно это уметь. В твоём enum-gen достаточно вызвать enum_generate_serialize второй раз только с флагом "сделай_специлизации_для_io" после ns-guard (а из первого вызова под ns-guard эти флаги убрать).
И при этом флаги у тебя уже есть — в остальном там реально в самом генераторе переделывать-то строчек 5


M>Генератор сильно переписать не вариант, более реальный вариант поправить что-то в макросной консерватории, которую использует генератор


Дело твоё, но ты выбираешь очень сложный путь.
Re[4]: ADL не ADL? Или как?
От: Marty Пират https://www.youtube.com/channel/UChp5PpQ6T4-93HbNF-8vSYg
Дата: 08.01.24 03:58
Оценка:
Здравствуйте, watchmaker, Вы писали:

M>>Генератор берёт пачку определений enum на входе и открытие/закрытие NS вставляет в начало и конец сгенерённого файла. Внутри он не умеет прыгать по разным NS туда-сюда, это кучу кода генератора надо переделывать.


W>А ему и не нужно это уметь. В твоём enum-gen достаточно вызвать enum_generate_serialize второй раз только с флагом "сделай_специлизации_для_io" после ns-guard (а из первого вызова под ns-guard эти флаги убрать).

W>И при этом флаги у тебя уже есть — в остальном там реально в самом генераторе переделывать-то строчек 5

ну поправь
Я уже наверное и не смогу найти, что там подпилить, мне таки проще матросиков поправить
https://github.com/al-martyn1/umba-tools
https://github.com/al-martyn1/umba-enum-gen




M>>Генератор сильно переписать не вариант, более реальный вариант поправить что-то в макросной консерватории, которую использует генератор


W>Дело твоё, но ты выбираешь очень сложный путь.


Поправить макросы в сорцах мне пока всё ещё кажется более простым путём, чем допиливать генератор и деплоить его всем нуждающимся
Маньяк Робокряк колесит по городу
Re[3]: ADL не ADL? Или как?
От: rg45 СССР  
Дата: 08.01.24 10:41
Оценка:
Здравствуйте, Marty, Вы писали:

M>Не стоило цитировать всю мою простыню, я до твоего ответа с трудом добрался


Пардон, я поправил. Там просто непонятно, что выкинуть, оно как-то все имеет отношение. Так-то я оверквотингом не злоупотребляю обычно.
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 08.01.2024 10:42 rg45 . Предыдущая версия .
Re: ADL не ADL? Или как?
От: Кодт Россия  
Дата: 08.01.24 17:37
Оценка:
Здравствуйте, Marty, Вы писали:

В порядке безумия: сделать обратный вывод типов.

// необязательно иметь такой концепт, и необязательно реализовывать его именно так - но он пригодится
template<class E> concept MyEnumFamily = requires { enum_deserialize(std::string_view(), E::defVal); };

struct enum_desu {
  std::string_view s;

  template<MyEnumFamily E> operator E() const { return enum_deserialize(s, E::defVal); }  // эксплуатируем ADL
};


и тогда
ns_a::enum_a a;
ns_b::enum_b b;
.....

a = ::enum_desu{sa};
b = ::enum_desu{sb};


https://gcc.godbolt.org/z/W5Pjbn75c
Перекуём баги на фичи!
Re: ADL не ADL? Или как?
От: K13 http://akvis.com
Дата: 08.01.24 17:50
Оценка:
M>Есть у меня сериализация/десериализация enum'ов.

M>Она на внешнем генераторе, в результате его работы в том же пространстве имён, в котором объявлен enum, появляется функция:


Зачем изобретать велосипед?

https://github.com/Neargye/magic_enum

Enum value to string


Color color = Color::RED;
auto color_name = magic_enum::enum_name(color);
// color_name -> "RED"


String to enum value


std::string color_name{"GREEN"};
auto color = magic_enum::enum_cast<Color>(color_name);
if (color.has_value()) {
  // color.value() -> Color::GREEN
}

// case insensitive enum_cast
auto color = magic_enum::enum_cast<Color>(value, magic_enum::case_insensitive);

// enum_cast with BinaryPredicate
auto color = magic_enum::enum_cast<Color>(value, [](char lhs, char rhs) { return std::tolower(lhs) == std::tolower(rhs); }

// enum_cast with default
auto color_or_default = magic_enum::enum_cast<Color>(value).value_or(Color::NONE);
Re[2]: ADL не ADL? Или как?
От: _NN_ www.nemerleweb.com
Дата: 08.01.24 21:06
Оценка: +1
Здравствуйте, 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.

В некоторых случаях это не проблема, но не для общего решения любого перечисления.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[3]: ADL не ADL? Или как?
От: K13 http://akvis.com
Дата: 09.01.24 05:51
Оценка:
Здравствуйте, _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
Re[4]: ADL не ADL? Или как?
От: watchmaker  
Дата: 09.01.24 16:16
Оценка:
Здравствуйте, K13, Вы писали:

K13>единственное реальное ограничение


Нужно быть честными: это всё же далеко не единственное серьёзное ограничение.

Даже Marty в первом сообщении показывает своё перечисление, в котором есть алиасы, которые в magic_enum не будут работать никак.


Да и magic_enum — это в первую очередь интересное техническое демо как хакнуть компилятор (как и другие magic_* демки), и лишь во вторую очередь что-то пригодное к использованию


K13> диапазон енумов должен влезать в UINT16_MAX


Это даже на современном железе будет давать плюс пару десятков секунд компиляции для каждого перечисления для каждой затронутой единицы трансляции, в которую транзитивно попадёт что-то, использующее библиотеку. И результат не кешируется между запусками: время придётся тратить при каждой компиляции даже если в перечислении ничего не поменялось, а поменялся какой-то другой код.

И в readme не просто так сразу упомянуто, что библиотека будет вызывать срабатывания watch-dog'ов во всех компиляторах, которые слезят за зависаниями компилятора.
Лимиты, конечно, можно увеличить. Но разрабатываться всё равно станет тяжело из-за длительной компиляции: ждать минуту прежде чем ide или компилятор подскажет, что, например, в имени переменной или в имени класса опечатка, — это уже кажется диким.



Понятно, что если в приложении всего пара крошечных перечислений, то библиотеку можно аккуратно использовать. Но раскручивать лимиты или добавлять в большой проект — скорее нет.


Главное преимущество библиотеки — для неё не нужно что-то делать в системе сборки. С чем в С++ есть большие исторические проблемы. И тут можно понять некоторых людей, которые готовы пожертвовать временем каждой компиляции, чтобы не разбираться как это сделать в cmake, meson, bazel или в чём-то другом.

Но если кодогенератор уже написан и используется, то переход на magic_enum — это почти со всех сторон пессимизация: меньше возможностей, замедляет компиляцию, хуже отслеживает необходимость пересборки, и имеет ограничения, которые никак не обойти, если в них упрёшся.
Re[2]: ADL не ADL? Или как?
От: B0FEE664  
Дата: 09.01.24 16:44
Оценка:
Здравствуйте, K13, Вы писали:

K13>https://github.com/Neargye/magic_enum


K13>
Color color = Color::RED;
K13>auto color_name = magic_enum::enum_name(color);
K13>// color_name -> "RED"

  dastish fantastisch
template <typename E>
constexpr auto n() noexcept {
  static_assert(is_enum_v<E>, "magic_enum::detail::n requires enum type.");

  if constexpr (supported<E>::value) {
#if defined(MAGIC_ENUM_GET_TYPE_NAME_BUILTIN)
    constexpr auto name_ptr = MAGIC_ENUM_GET_TYPE_NAME_BUILTIN(E);
    constexpr auto name = name_ptr ? str_view{name_ptr, std::char_traits<char>::length(name_ptr)} : str_view{};
#elif defined(__clang__)
    str_view name;
    if constexpr (sizeof(__PRETTY_FUNCTION__) == sizeof(__FUNCTION__)) {
      static_assert(always_false_v<E>, "magic_enum::detail::n requires __PRETTY_FUNCTION__.");
      return str_view{};
    } else {
      name.size_ = sizeof(__PRETTY_FUNCTION__) - 36;
      name.str_ = __PRETTY_FUNCTION__ + 34;
    }
#elif defined(__GNUC__)
    auto name = str_view{__PRETTY_FUNCTION__, sizeof(__PRETTY_FUNCTION__) - 1};
    if constexpr (sizeof(__PRETTY_FUNCTION__) == sizeof(__FUNCTION__)) {
      static_assert(always_false_v<E>, "magic_enum::detail::n requires __PRETTY_FUNCTION__.");
      return str_view{};
    } else if (name.str_[name.size_ - 1] == ']') {
      name.size_ -= 50;
      name.str_ += 49;
    } else {
      name.size_ -= 40;
      name.str_ += 37;
    }
#elif defined(_MSC_VER)
    // CLI/C++ workaround (see https://github.com/Neargye/magic_enum/issues/284).
    str_view name;
    name.str_ = __FUNCSIG__;
    name.str_ += 40;
    name.size_ += sizeof(__FUNCSIG__) - 57;
#else
    auto name = str_view{};
#endif
    std::size_t p = 0;
    for (std::size_t i = name.size_; i > 0; --i) {
      if (name.str_[i] == ':') {
        p = i + 1;
        break;
      }
    }
    if (p > 0) {
      name.size_ -= p;
      name.str_ += p;
    }
    return name;
  } else {
    return str_view{}; // Unsupported compiler or Invalid customize.
  }
}
И каждый день — без права на ошибку...
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.