вторник, 25 августа 2009 г.

Создание класса сериализующегося с использованием различных потоков



При программировании на С++ иногда приходится сталкиваться с необходимостью реализовать в классе сериализацию в c использованием различных потоков, а не только с иcпользованием iostream. Для начала приведу пример сериализации ставший классическим:
#include <iostream>
#include <string>

class User
{
public:
User() : m_id(-1), m_balance(0)
{
}
User(long id,
std::string& name,
std::string& surname,
double balance) :
m_id(id),
m_name(name),
m_surname(surname),
m_balance(balance)
{
}

void setName(std::string& name) { m_name = name; }
std::string name() const { return m_name; }
void setSurname(std::string& surname) { m_surname = surname; }
std::string surname() const { return m_surname; }
void setBalance(double balance) { m_balance = balance; }
long id() const { return m_id; }

friend std::ostream& operator << (std::ostream& out, const User& user)
{
return out << m_id
<< m_name
<< m_surname
<< m_balance;
}
friend std::istream& operator >> (std::istream& in, const User& user)
{
in >> m_id
>> m_name
>> m_surname
>> m_balance;
return in;
}
private:
long m_id;
std::string m_name;
std::string m_surname;
double m_balance;
};



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



#include "User.h"
#include <iostream>
#include <fstream>

int main()
{
User u_(121001, "User1", "UserSurname", 123);
std::ofstream fout;
if (fout.open("test.dat", std::ios::out|std::ios::binary))
fout << u_;
fout.close();


std::ifstream fin;
User _u;
if (fout.open("test.dat", std::ios::out|std::ios::binary))
fin << _u;
fin.close();

cout << _u << endl;

}



Во многих случаях этого вполне достаточно, однако у такого подхода есть несколько минусов:
- Если есть иерархия классов, то дружественные операторы придется перегружать для каждого класса иерархии
- Сложно использовать другие средства сериализации (например, может потребоваться сериализация в xml), или точнее для каждого класса иерархии уже придется дописывать еще по два перегруженных оператора (при этом 100% где-то что-то забудешь).

Существует элегантный способ сделать сериализацию еще более гибкой, так что предлагаю на суд читателей свое изобретение:


//Файл iserializable.hpp
#ifndef ISERIALIZABLE_HPP
#define ISERIALIZABLE_HPP

#ifndef interface
#define interface struct
#endif

template <typename _Stream>
interface ISerializable
{
virtual void pack(_Stream& out) const = 0;
virtual void unpack(_Stream& in) = 0;

};

#endif


//Файл User.h

#ifndef USER_H
#define USER_H


#include <iostream>

// Следующие два заголовка
// из библиотеки Qt
#include <QTextStream>
#include <QDataStream>


class User :
public ISerializable<std::iostream>,
public ISerializable<QTextStream>,
public ISerializable<QDataStream>
{
public:
User() : m_id(-1), m_balance(0){}
User(long id,
std::string& name,
std::string& surname,
double balance) :
m_id(id),
m_name(name),
m_surname(surname),
m_balance(balance)

{
}

virtual void setName(std::string& name) { m_name = name; }
virtual std::string name() const { return m_name; }
virtual void setSurname(std::string& surname) { m_surname = surname; }
virtual std::string surname() const { return m_surname; }
virtual void setBalance(double balance) { m_balance = balance; }

long id() const { return m_id; }

virtual void pack(std::iostream& out) const
{
out << m_id << m_name << m_surname << m_balance;
}

virtual void unpack(std::iostream& in)
{
in >> m_id >> m_name >> m_surname >> m_balance;
}

virtual void pack(QTextStream& out) const
{
out << m_id << m_name << m_surname << m_balance;
}
virtual void unpack(QTextStream& in)
{
in >> m_id >> m_name >> m_surname >> m_balance;
}

virtual void pack(QDataStream& out) const
{
out << m_id << m_name << m_surname << m_balance;
}
virtual void unpack(QDataStream& in)
{
in >> m_id >> m_name >> m_surname >> m_balance;
}


private:

long m_id;
std::string m_name;
std::string m_surname;
double m_balance;

};



class AdvancedUser : public User

{

public:

AdvancedUser() : User()
{
}

AdvancedUser( long id,
std::string& name,
std::string& surname,
double balance,
std::string& advanced) :
User(id, name, surname, balance),
m_advancedData(advanced)

{
}

void setAdvancedData(std::string& dat) { m_advancedData = dat; }
std::string advancedData() const { return m_advancedData; }

virtual void pack(iostream& out) const
{
User::unpack(in);
out << advancedData;
}



virtual void unpack(iostream& in)
{
User::pack(out);
in >> m_advancedData;
}



virtual void pack(QTextStream& out) const
{
User::pack(out);
out << advancedData;
}



virtual void unpack(QTextStream& in)
{
User::unpack(in);
in >> m_advancedData;
}



virtual void pack(QDataStream& out) const
{
User::pack(out);
out << advancedData;
}


virtual void unpack(QDataStream& in)
{
User::unpack(in);
in >> m_advancedData;
}


private:

std::string m_advancedData;
};


#endif




Идея предлагаемого подхода состоит в следующем: создается абстрактный шаблонный интерфейс, который должен быть инстанциирован по потоку. Далее наследуем наш класс от этого интерфейса и реализуем функции чтения и записи. Казалось бы все просто? Ан, нет... При наследовании от интерфейса мы обязаны явно указать параметры шаблона. Магия в том, что интерфейс написан один раз, и наследуемся мы вроде бы тоже от одного класса, а в реальности получается множественное наследование! В некотором смысле в приведенном примере используется статический полиморфизм, однако мы обязаны реализовать теперь не 2 функции а 6 (по 2 на каждый инстанциированный шаблон). Некоторые могут возразить что приведенный пример не скомпилируется из-за того что имена функций чтения/записи одинаковы, но это не совсем так: просто получается, что мы делаем эти функции перегруженными.
Другие читатели могут заметить, что возникнут проблемы если два раза пронаследоваться от интерфейса проинстанциированным по одному и тому же типу потока. Сразу хочу заметить: такое не скомпилируется (ошибка времени компиляции: уже здорово, гораздо лучше чем ошибка времени выполнения), и кроме того: часто ли вы по ошибке наследуетесь от одного и того же класса? )))
А вот самые внимательные читатели обнаружат, что начал я с прегрузки операторов вводо-вывода в поток, что же с ними? Тут очередной (хотя и вполне очевидный) трюк: перегрузить операторы ввода-вывода потребуется только один раз! Действительно, поскольку класс User в нашем примере полиморфный, то операторы ввода-вывода можно сделать глобальными и описать следующим образом:


std::iostream& operator << (const User& usr, std::iostream& out)
{ usr.pack(out); }

std::iostream& operator >> (const User& usr, std::iostream& in)
{ usr.unpack(in); }



QTextStream& operator << (const User& usr, QTextStream& out)
{ usr.pack(out); }

QTextStream& operator >> (const User& usr, QTextStream& in)
{ usr.unpack(in); }



QDataStream& operator << (const User& usr, QDataStream& out)
{ usr.pack(out); }

QDataStream& operator >> (const User& usr, QDataStream& in)
{ usr.unpack(in); }


Комментариев нет: