При программировании на С++ иногда приходится сталкиваться с необходимостью реализовать в классе сериализацию в 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% где-то что-то забудешь).
Существует элегантный способ сделать сериализацию еще более гибкой, так что предлагаю на суд читателей свое изобретение:
#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
#ifndef USER_H
#define USER_H
#include <iostream>
#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); }