Статичні відображення

Часто доводиться стикатися такими моментами, як відображення перерахування в рядок і назад, або фіксоване значення для подальшої серіалізації. Тут приводитися спосіб однозначної статичного відображення значення типу A в значення типу B і назад. Я спробував наділити метод невеликий гнучкістю, що дозволяє легко розширювати варіанти відображень.

В цілому суть методу полягає в тому, що є статичний контейнер — карта відображень — а так же ряд допоміжних функцій, розширюваних під потреби проекту і позбавляють від прямої взаємодії з контейнером.

В результаті перетворення вихідного типу в рядок і назад буде виглядати так:

// Вихідний тип
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( const string& 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"



Застосовую цю техніку у своїх проектах — дуже зручно. Напевно є ідеї щодо поліпшення — тут я виклав основний підхід.

Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.