Pull to refresh

Удобная сериализация данных с Variadic Templates

Reading time 9 min
Views 10K

Предисловие


В ходе разработки своего проекта мне потребовалось иметь возможность записывать содержимое различных структур данных в бинарные файлы, и, так как зачастую в них присутствовали строки, векторы и прочие данные, меняющие свой размер во время выполнения, каждая такая структура требовала индивидуального подхода к организации ее перевода в последовательность байт, пригодную для обратного считывания, а использование boost мне показалось громоздким (да и нет его у меня), да и хотелось самому решить эту задачу. Из-за этого я решил сделать этот процесс как можно менее рутинным и делать я это решил с помощью шаблонов.

Поддерживаются следующие типы данных:
— Все фундаментальные типы С++
— std::string
— std::vector где T — все что угодно из этого же списка
— Любой перечислимый тип

Релизация


В качестве среды разработки я использую Visual Studio 2013, но код решения является кросс-платформенным. Класс, отвечающий за весь функционал я назвал AbstractSaveData. Используется он с помощью наследования. Я решил не делать сам класс шаблонным, так как это сделает его использование довольно не удобным, а в заголовке все таки это слово фигурирует. Вместо этого шаблонными будут только его методы и, таким образом, при использовании этого класса не придется ни разу явно инстанцировать ни один шаблонный метод.

Интерфейс класса составляют следующие методы:
virtual void const* Serialize(int& size) = 0;
virtual void Deserialize(const void* buf, size_t size) = 0;
virtual int SerializedSize()const = 0;
void CleanSerializedBuffer();

Реализация первых трех методов должна быть в классе-потомке.

Метод CleanSerializedBuffer используется для очистки локального буфера с сериализованными данными. В реализации ничего особенного:
void CleanSerializedBuffer()
{
	delete[] serializedBuf;
	serializedBuf = nullptr;
	m_size = 0;
}

Но это только то, что касается public-методов. Классу-потомку, чьи данные подлежат сериализации, предстоит иметь дело со следующими, protected-методами:
template<class ...Ts>
int Serialization(const Ts&... objects);
template<class ...Ts>
void Deserialization(const void* buf, size_t size, Ts&... objects);
template<class T, class ...Ts>
inline int CalculateSize(const T& obj, const Ts&... objects)const;
const char* SerializedBuf()const;

Метод Serialization как не трудно догадаться осуществляет сериализацию. Реализация метода:
template<class ...Ts>
int Serialization(const Ts&... objects)
{
	if (serializedBuf)
		delete[] serializedBuf;
	m_size = CalculateSize(objects...);
	serializedBuf = new char[m_size];
	
	ProcessSerialization(0, objects...);

	return m_size;
}

Сначала вычисляется размер буфера, необходимый для данных и выделяется соответствующее количество памяти. Затем происходит уже непосредственно сериализация данных.

Реализация метода Deserialization по своей структуре схожа с предыдущим:
template<class ...Ts>
void Deserialization(const void* buf, size_t size, Ts&... objects)
{
	if (size)
	{
		int read = CalculateSize(buf, objects...);
		if (read != size)
			throw ApproxException(L"Ошибка десериализации");
		ProcessDeserialization(static_cast<const char*>(buf), objects...);
	}
}

Только здесь вычисление размера нужно только для контроля ошибок.

И наконец, метод CalculateSize, занимающийся расчетом занимаемого объектами места, он имеет 2 варианта:
template<class T>
inline int CalculateSize(const T& obj)const
{
	return reqSize(obj);
}
template<class T, class ...Ts>
inline int CalculateSize(const T& obj, const Ts&... objects)const
{
	return reqSize(obj) + CalculateSize<Ts...>(objects...);
}

Здесь уже можно наблюдать рекурсию как во время компиляции, так и во время выполнения. Знакомым с кортежами в C++ не составит труда понять что здесь происходит. Стоит упомянуть, что всего имеется 4 реализации данного метода, остальные две находятся в private-секции и не вызываются непосредственно из класса-наследника, но вызываются в процессе десериализации.

Ну, и метод SerializedBuf просто возвращает указатель на серилизованные данные:
const char* SerializedBuf()const
{
	return serializedBuf;
}

И, наконец, то что как говорится «под капотом».

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

Всего здесь есть 3 группы методов:

Первая: рекурсивные. Они обеспечивают раскрытие списка аргументов, перемещение по буферу и вызов методов производящих обработку объектов в соответствии с их типом. Это методы с именами CalculateSize, ProcessSerialization, ProcessDeserialization.

Вторая: копирующие. Они осуществляют сериализацию или десериализацию на уровне отдельного объекта и копируют полученное в буфер или из него. Это методы с именами CopyS и CopyD. Методы с именами CopyS используются в процессе сериализации, а CopyD — в десериализации.

Третья: вспомогательные. Они производят расчет занимаемого места на уровне отдельного объекта. Это методы с именем reqSize.
В коде активно применяется явная специализация шаблонов, а так же средства стандартной библиотеки проверки типов std::is_fundamental, std::is_enum и std::is_base_of и вместе с ними std::enable_if. Эти средства позволили отделить объекты с постоянным размером от объектов с переменным размером. Я для наглядности и простоты написал свое средство проверки типа основанное на стандартных:
template<typename T>
struct is_simple
	: std::_Cat_base<std::is_fundamental<T>::value || std::is_enum<T>::value>
{
};

Оно просто объединяет множество фундаметальных типов и типов перечислений, что в нашем случае весьма удобно так как в обоих случаях по типу объекта можно однозначно узнать какого он размера. Для удобства далее будем называть эти типы простыми.
В общем, здесь представлена самая обычная сериализация — в начале 4 байта хранят размер, а затем идут сами данные. Исключение составляют простые типы, им не требуется заголовок, так как информация о их размере соответствует их типу и предоставляется классом-наследником при десериализации. Таким образом уменьшается избыточность данных.

Отдельного внимания следует уделить массивам(векторам). Метод сериализации массива выбирается на основе типа данных, которые он содержит. В данной реализации может произойти 2 варианта: массив простых типов и массив типов, производных от AbstractSaveData. Обычные структуры не поддерживаются, попытка их использования приведет к ошибке компиляции, но их внедрение не проблема, просто в моем проекте это не требуется, к тому же, их применение снимает гарантию успешной сериализации, так как их содержимое неизвестно и может быть любым (указатели, те же строки и векторы), и вместо них можно использовать структуру, с наследованием от AbstractSaveData.

Это, пожалуй, все что я мог рассказать в теории. Вот код:
template<class T>
inline void ProcessDeserialization(const char* buf, T& obj)
{
	CopyD(buf, obj);
}

template<class T, class ...Ts>
inline void ProcessDeserialization(const char* buf, T& obj, Ts&... objects)
{
	ProcessDeserialization<Ts...>(buf + CopyD(buf, obj), objects...);
}

template<class T>
inline void ProcessSerialization(int shift, const T& obj)
{
	CopyS(shift, obj);
}
template<class T, class ...Ts>
inline void ProcessSerialization(int shift, const T& obj, const Ts&... objects)
{
	shift += CopyS(shift, obj);
	ProcessSerialization<Ts...>(shift, objects...);
}
	//Copy Serialization methods begin
template<typename saveData>
inline typename std::enable_if<std::is_base_of<AbstractSaveData, saveData>::value, int>::type CopyS(int shift, saveData& obj)
{
	AbstractSaveData* data = dynamic_cast<AbstractSaveData*>(&obj);
	int size;
	auto ptr = data->Serialize(size);
	size += sizeof(int);
	memcpy(serializedBuf + shift, &size, sizeof(int));
	memcpy(serializedBuf + shift + sizeof(int), ptr, size - sizeof(int));
	data->CleanSerializedBuffer();
	return size;
}

template<typename T>
inline typename std::enable_if<is_simple<T>::value,int>::type CopyS(int shift, const T& obj)
{
	memcpy(serializedBuf + shift, &obj, sizeof(T));
	return sizeof(T);
}
inline int CopyS(int shift, const std::string& obj)
{
	int size = reqSize(obj);
	memcpy(serializedBuf + shift, &size, sizeof(int));
	memcpy(serializedBuf + shift + sizeof(int), obj.c_str(), size - sizeof(int));
	return size;
}

inline int CopyS(int shift, const std::pair<const void*, int>& obj)
{
	memcpy(serializedBuf + shift, &obj.second, sizeof(int));
	memcpy(serializedBuf + shift + sizeof(int), obj.first, obj.second);
	return obj.second + sizeof(int);
}

template<class T>
inline typename std::enable_if<is_simple<T>::value, int>::type CopyS(int shift, const std::vector<T>& obj)
{
	int size = reqSize(obj);
	memcpy(serializedBuf + shift, &size, sizeof(int));
	memcpy(serializedBuf + shift + sizeof(int), obj.data(), size - sizeof(int));
	return size;
}
template<class T>
inline typename std::enable_if<!is_simple<T>::value, int>::type CopyS(int shift, const std::vector<T>& objects)
{
	int size = reqSize(objects);
	memcpy(serializedBuf + shift, &size, sizeof(int));
	for (auto obj : objects)
	{
		shift += CopyS(shift + sizeof(int), obj);
	}
	return size;
}

//Copy Serialization methods end

//Copy Deserialization methods begin
template<class T>
inline typename std::enable_if<is_simple<T>::value, int>::type CopyD(const void* buf, T& obj)
{
	memcpy(&obj, buf, sizeof(T));
	return sizeof(T);
}

inline int CopyD(const void* buf, std::string& obj)
{
	int size = *static_cast<const int*>(buf) - sizeof(int);
	obj.reserve(size);
	obj.assign(size, '0');
	memcpy(&obj[0], static_cast<const char*>(buf)+sizeof(int), size);
	return size + sizeof(int);
}

template<typename T>
inline typename std::enable_if<is_simple<T>::value, int>::type CopyD(const void* buf, std::vector<T>& obj)
{
	int size = *static_cast<const int*>(buf)-sizeof(int);
	obj.reserve(size / sizeof(T));
	obj.assign(obj.capacity(), 0);
	memcpy(obj.data(), static_cast<const char*>(buf) + sizeof(int) , size);
	return size + sizeof(int);
}

template<typename T>
inline typename std::enable_if<!is_simple<T>::value, int>::type CopyD(const void* buf, std::vector<T>& objects)
{
	int size = *static_cast<const int*>(buf);
	int remainedSize = size - sizeof(int);
	while (remainedSize != 0)
	{
		T obj;
		remainedSize -= CopyD(static_cast<const char*>(buf) + size - remainedSize, obj);
		objects.push_back(obj);
		if (remainedSize < 0)
		     throw ApproxException(L"Ошибка десериализации при работе с вектором.");
	}
	return size;
}

template<typename saveData>
inline typename std::enable_if<std::is_base_of<AbstractSaveData, saveData>::value, int>::type CopyD(const void* buf, saveData& obj)
{
	AbstractSaveData* data = dynamic_cast<AbstractSaveData*>(&obj);
	const int size = *static_cast<const int*>(buf);
	data->Deserialize(static_cast<const char*>(buf)+sizeof(int), size - sizeof(int));
	return size;
}
//Copy Deserialization methods end
template<typename simpleType>
inline typename std::enable_if<is_simple<simpleType>::value, int>::type reqSize(simpleType)const
{
	return sizeof(simpleType);
}
		
template<typename simpleType>
inline typename std::enable_if<is_simple<simpleType>::value, int>::type reqSize(const void*, simpleType)const
{
	return sizeof(simpleType);
}

template<typename saveData>
inline typename std::enable_if<std::is_base_of<AbstractSaveData, saveData>::value, int>::type reqSize(const saveData& Data)const
{
	return Data.SerializedSize() + sizeof(int);
}

inline int reqSize(const std::string& obj)const
{
	return obj.size() + sizeof(int);
}

template<class T>
inline typename std::enable_if<is_simple<T>::value, int>::type reqSize(const std::vector<T>& obj)const
{
	return obj.size() * sizeof(T) + sizeof(int);
}

template<class T>
inline typename std::enable_if<!(is_simple<T>::value), int>::type reqSize(const std::vector<T>& objects)const
{
	int res = 0;
	for (auto obj : objects)
	{
		res += reqSize(obj);
	}
	return res + sizeof(int);
}

inline int reqSize(const std::pair<const void*, int>& obj)const
{
	return obj.second + sizeof(int);
}

template<typename notSimpleType>
inline typename std::enable_if<!(is_simple<notSimpleType>::value), int>::type reqSize(const void* buf, const notSimpleType&)const
{
	return *static_cast<const int*>(buf);
}

template<class T>
inline int CalculateSize(const void* buf, const T& obj)const
{
	return reqSize(buf, obj);
}
template<class T, class ...Ts>
inline int CalculateSize(const void* buf, const T& obj, const Ts&... objects)const
{
	int shift = reqSize(buf, obj);
	return shift + CalculateSize<Ts...>(static_cast<const char*>(buf) + shift, objects...);
}

Пример использования


И ради чего все это было? Чтобы можно было написать вот так:
using std::string;
	struct ShaderPart : AbstractSaveData
	{
		string Str_code;
		Shader_Type Shader_Type = ST_NONE;
		string EntryPoint;
		vector<RuntimeBufferInfo> BuffersInfo;
		vector<int> ParamsIDs;
		vector<int> TextureSlots;
		
		const void* Serialize(int& size)override final
		{
			size = Serialization(Str_code, Shader_Type, EntryPoint, BuffersInfo, ParamsIDs, TextureSlots);
			return SerializedBuf();
		}
		void Deserialize(const void* buf, size_t size)override final
		{
			Deserialization(buf, size, Str_code, Shader_Type, EntryPoint, BuffersInfo, ParamsIDs, TextureSlots);
		}
		int SerializedSize()const override final
		{
			return CalculateSize(Str_code, Shader_Type, EntryPoint, BuffersInfo, ParamsIDs, TextureSlots);
		}
	};

В принципе, чтобы не писать одни и те же переменные в методы и уменьшить вероятность несоответствия списков параметров(что приведет к весьма плохим последствиям), можно ввести поддержку кортежей и вместо передачи списка аргументов передавать всем методам один кортеж.

Всем кто уделил время моей статье — моя благодарность, а тем кто смог дотерпеть до конца еще и уважение.
Tags:
Hubs:
+8
Comments 13
Comments Comments 13

Articles