Часто приходится сталкиваться в такими моментами, как отображение перечисления в строку и обратно, либо в фиксированное значения для дальнейшей сериализации. Здесь приводиться способ однозначного статического отображения значения типа A в значение типа B и обратно. Я попытался наделить метод небольшой гибкостью, позволяющей легко расширять варианты отображений.
В кратце суть метода состоит в том, что имеется статический контейнер — карта отображений — а так же ряд вспомогательных функций, расширяемых под нужды проекта и избавляющих от прямого взаимодействия с контейнером.
В итоге преобразование из исходного типа в строку и обратно будет выглядеть так:
Чтобы функционал отображения стал уникальным — необходимо обобщить интерфейс взаимодействия с контейнерами.
Первое, что нам понадобиться — перегруженная функция доступа к карте отображений по исходному типу.
Второе, что нам понадобиться — интерфейс и организация самого контейнера отображений.
Для отображения в строку необходимо расширить поля структуры VievMap::View. Дополнительные поля я называю вариантами. Вот как выглядит готовый шаблон контейнера:
Как видно, StringViewMap наследует весь базовый функционал ViewMap, расширяя его вспомогательной функцией extractString.
Теперь реализовать функционал toString, stringTo очень просто:
Весь секрет toString и stringTo в использовании интерфейса контейнера — а именно его фукнций extractOrigin и extractString. Таким образом stringTo, toString будет работать только с теми отображениями, что предоставляют интерфейс extractString.
ViewMapTraits необходим ввиду того, что сигнатура перегруженной функции viewMapOf может отличаться для разных перегрузок, а именно возвращаемое значение, может быть как ссылкой так и объектом. Вот каков он внутри:
И, наконец, реализация viewMapOf для перечисления Fruit:
Весь базовый функционал готов. Я покажу как добавить дополнительные варианты отображения на примере нового перечисления:
Здесь добавили дополнительную функцию — классификатор toFruit. Смысл ее тот же, что и у toString, изменилось немного содержание. Теперь продемонстрирую работу преобразований:
Применяю данную технику в своих проектах — очень удобно. Наверняка есть идеи по улучшению — здесь я изложил основной подход.
Преимущества данного подхода:
Недостатки:
UPD: Изменил заголовок на более подходящий и добавил преимущества и недостатки подхода
В кратце суть метода состоит в том, что имеется статический контейнер — карта отображений — а так же ряд вспомогательных функций, расширяемых под нужды проекта и избавляющих от прямого взаимодействия с контейнером.
В итоге преобразование из исходного типа в строку и обратно будет выглядеть так:
// Исходный тип enum class Fruit{ Unknown, Apple, Banana, Orange, }; // Преобразование в строку string fruitStr = toString(Fruit::Orange); // Обратное преобразование из строки в исходный тип Fruit fruit = stringTo<Fruit>(fruitStr);
Чтобы функционал отображения стал уникальным — необходимо обобщить интерфейс взаимодействия с контейнерами.
Первое, что нам понадобиться — перегруженная функция доступа к карте отображений по исходному типу.
// Вспомогательный хранитель типа для разрешения перегрузки template<typename> struct TypeHolder {}; // Сигнатура функции получения доступа к контейнеру отображения ContainerType const& viewMapOf(TypeHolder<Fruits>);
Второе, что нам понадобиться — интерфейс и организация самого контейнера отображений.
template<typename SourceT, typename VariantsT> struct ViewMap { // Исходный тип using SourceType = SourceT ; // Структура отображения struct View: VariantsT { View(SourceT id=SourceT(), VariantsT vnt=VariantsT()): VariantsT(vnt), origin(id) { ;; } bool operator<(View const& b) const { return origin < b.origin ; } SourceT origin ; //< Исходное значение }; using Views = std::set<View> ; ViewMap() { ;; } ViewMap(std::initializer_list<View> const& initViews, View const& initInvalidView): views(initViews), invalidView(initInvalidView) { ;; } // Получение исходного типа static SourceT extractOrigin(View const& view) { return view.origin ; } Views views; // Карта отображений View invalidView; // Отображение ошибки } ;
Для отображения в строку необходимо расширить поля структуры VievMap::View. Дополнительные поля я называю вариантами. Вот как выглядит готовый шаблон контейнера:
struct StringVariant { StringVariant(std::string const& s = ""): str(s) { ;; } std::string str ; }; // Карта отображений в строку template< typename SourceT > struct StringViewMap: ViewMap<SourceT,StringVariant> { using Base = ViewMap<SourceT, StringVariant>; using View = typename Base::View; StringViewMap() { ;; } StringViewMap(std::initializer_list<View> const& mapInit, View const& invalidInit): Base(mapInit,invalidInit) { ;; } // Извлечение строки из отображения static std::string const& extractString(typename Base::View const& view) { return view.str ; } } ;
Как видно, StringViewMap наследует весь базовый функционал ViewMap, расширяя его вспомогательной функцией extractString.
Теперь реализовать функционал toString, stringTo очень просто:
template<typename SourceType> string toString(SourceType id) { using MapRef = typename ViewMapTraits<SourceType>::MapRef; using PureMap = typename ViewMapTraits<SourceType>::PureMap; MapRef map = viewMapOf(TypeHolder<SourceType>()) ; auto const& views = map.views ; auto pos = views.find(typename PureMap::View(id)) ; return PureMap::extractString( (pos != views.end()) ? *pos : map.invalidView ) ; } template<typename SourceType> SourceType stringTo( string const& str ) { using MapRef = typename ViewMapTraits<SourceType>::MapRef; using PureMap = typename ViewMapTraits<SourceType>::PureMap; MapRef map = viewMapOf(TypeHolder<SourceType>()) ; auto const& views = map.views ; auto pos = std::find_if( views.begin(), views.end(), [&](typename PureMap::View const& val) { return PureMap::extractString(val) == str ; } ) ; return PureMap::extractOrigin( (pos != views.end()) ? *pos : map.invalidView ) ; }
Весь секрет toString и stringTo в использовании интерфейса контейнера — а именно его фукнций extractOrigin и extractString. Таким образом stringTo, toString будет работать только с теми отображениями, что предоставляют интерфейс extractString.
ViewMapTraits необходим ввиду того, что сигнатура перегруженной функции viewMapOf может отличаться для разных перегрузок, а именно возвращаемое значение, может быть как ссылкой так и объектом. Вот каков он внутри:
template<typename SourceType> struct ViewMapTraits { using MapRef = decltype( mapViewOf(TypeHolder<SourceType>()) ) ; using PureMap = typename std::remove_cv<typename std::remove_reference<MapRef>::type>::type ; };
И, наконец, реализация viewMapOf для перечисления Fruit:
StringViewMap<Fruit> const& viewMapOf(TypeHolder<Fruit>) { static StringViewMap<Fruit> viewMap = { { {Fruit::Apple, {"apple"}}, {Fruit::Banana, {"banana"}}, {Fruit::Orange, {"orange"}}, }, {Fruit::Unknown, {"unknown"}} }; return viewMap ; }
Весь базовый функционал готов. Я покажу как добавить дополнительные варианты отображения на примере нового перечисления:
enum class Mix { Unknown, RedApple, GreenApple, GreenBanana, BigOrange, SmallOrange, }; // Варианты отображения для Mix struct MixVariant { MixVariant(Fruit f = Fruit::Unknown, std::string const& s = ""): fruit(f), str(s) { ;; } Fruit fruit ; // Классификация фрукта std::string str ; // Строковое представление }; // Карта отображений struct MixViewMap: ViewMap<Mix,MixVariant> { using Base = ViewMap<Mix,MixVariant>; using View = typename Base::View; MixViewMap() { ;; } MixViewMap(std::initializer_list<View> const& mapInit, View const& invalidInit): Base(mapInit,invalidInit) { ;; } // Интерфейс для toString, stringTo static std::string const& extractString(typename Base::View const& view) { return view.str ; } // Интерфейс для toFruit static std::string const& extractFruit(typename Base::View const& view) { return view.fruit ; } } ; // Заполняем карту MixViewMap const& viewMapOf(TypeHolder<Mix>) { static MixViewMap map = { { {Mix::RedApple, {Fruit::Apple, "red_apple"}}, {Mix::GreenApple, {Fruit::Apple, "green_apple"}}, {Mix::GreenBanana, {Fruit::Banana, "green_banana"}}, {Mix::BigOrange, {Fruit::Orange, "big_orange"}}, {Mix::SmallOrange, {Fruit::Orange, "small_orange"}}, }, {Mix::Unknown, {Fruit::Unknown, "unknown"}}, }; return map ; } // Вспомогательная функция классификации template<typename SourceType> Fruit toFruit(SourceType id) { using MapRef = typename ViewMapTraits<SourceType>::MapRef; using PureMap = typename ViewMapTraits<SourceType>::PureMap; MapRef map = viewMapOf(TypeHolder<SourceType>()) ; auto const& views = map.views ; auto pos = views.find(typename PureMap::View(id)) ; return PureMap::extractFruit( (pos != views.end()) ? *pos : map.invalidView ) ; }
Здесь добавили дополнительную функцию — классификатор toFruit. Смысл ее тот же, что и у toString, изменилось немного содержание. Теперь продемонстрирую работу преобразований:
string redAppleStr = "red_apple"; Mix mix = stringTo<Mix>(redAppleStr); // mix == Mix::RedApple Fruit mixFruit = toFruit(mix); // mixFruit == Fruit::Apple string mixFruitStr = toString(mixFruit); // mixFruitStr == "apple"
Применяю данную технику в своих проектах — очень удобно. Наверняка есть идеи по улучшению — здесь я изложил основной подход.
Преимущества данного подхода:
- Отображения значения любого типа(перечисления — частный случай)
- Множественные варианты отображений для исходного значения
- Совместимость расширенных контейнеров(MixViewMap) с уже написанным функционалом(toString,stringTo)
- Не задействуются макросы, ввиду чего словари не раскрываются в местах определения перечислений(актуально для содержания заголовков «чистоте» — например API библиотеки)
Недостатки:
- Небольшое утомление при заполнении карты отображений
UPD: Изменил заголовок на более подходящий и добавил преимущества и недостатки подхода
