Расширяемые отображения любых типов значений без использования макросов

в 6:21, , рубрики: c++

Часто приходится сталкиваться в такими моментами, как отображение перечисления в строку и обратно, либо в фиксированное значения для дальнейшей сериализации. Здесь приводиться способ однозначного статического отображения значения типа 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( 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"

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

Автор: saydex

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js