вторник, 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); }


понедельник, 24 августа 2009 г.

Блокирование наследования в С++

Представим на минуту что мы пишем инструментальную библиотеку, в которой есть менеджер сессий входа в систему, причем реализовать его требуется как синглтон и кроме того заблокировать возможность наследования от этого класса, поскольку пр наследовании и переопределении методов менеджера программа может работать неустойчиво или попросту падать.
Итак перед нами интересный вопрос на эрудицию: как заблокировать наследование от класса в С++? Напомню, что в C# и Java такая возможность существует. Как же ее реализовать в С++?
Многие скорее всего кинуться делать макросы или просто попытаются пихнуть в базовый класс какую-нибудь статическую переменную для проверки пронаследован класс или нет. Однако это не поможет. При та5ком подходе мы упускаем одну очень важную деталь: все эти проверки будут происходить во времяы выполнения и кроме того всегда возвращать неверные значения, поскольку по правилам C++ конструктор базового класса ВСЕГДА вызывается первым.
Так как же быть?
Ответ настолько же прост насколько и сложен: необходимо перенести деструктор класса в private часть класса. Читатель может возразить: при чем же здесь деструктор?

Предлагаю взглянуть на следующий фрагмент кода:


class SessionManager
{
public:
bool login(std::string& name, std::string& password);
void logout();
static bool create();
static bool destroy();
static SessionManager* instance();
private:
static SessionManager* m_self;
// блокируем констрктор
SessionManager();
// блокируем деструктор!!!
~SessionManager();
// блокируем оператор присваивания
SessionManager& operator = (const SessionManager&);
};

class SessionManagerDerived : public SessionManager
{
............
............
static bool create() {
// ошибка времени компиляции:
// невозможно создать объект класса с закрытым деструктором!
if (!m_self) return new SessionManagerDerived();
return m_self;
}
};



Вот и вся фишка: компилятор не позволит создать класс с закрытым деструктором, поскольку не знает как его удалять! Замечу что для класса SessionManager компилятор имеет эти данные хоть они и в private части.

вторник, 24 февраля 2009 г.

Всем доброго времени суток!

Как обещал продолжаю тему рецептов.
Так как речть в предыдущих статьях в основном шла о библиотеках Qt, то дальнейший код будет приведен с использованием классов из этой библиотеки. Недавно столкнулся с такой задачей: есть приложение, довольное большое. Требуется описать понятие проекта для организации разнообразных файлов и настроек. Таким образом получается некая древовидная структура, например:

Проект
Текстовые файлы
Файл1.txt
Файл2.txt
Файл3.doc
Рисунки
Рис1.jpg
Рис2.bmp
Рис3.png
......
......

Логично что во время выполнения программы должен существовать один единственный проект. В большинстве случаев в такой ситуации используют шаблон Singletone. Да,это не плохое решение, но я пошел немного дальше. Известно, что при написании любой программы на Qt экземпляров класса QApplication также не может быть более одного. Но вернемся к проекту, описать эту абстракцию можно следующим образом:

class QProject // класс проекта
{
public:
Q_DISABLE_COPY(QProject) // блокируем копирование объекта

// добавить файл в проект
void appendFile(const QString& filename)
{
if (!m_filesList.contains(filename))
m_filesList.append(filename)
}
// удалить файл из проекта
void removeFile(const QString& filename)
{
int index = m_filesList.indexOf(filename));
if (index != -1)
m_filesList.remove(index);
}
void load(constQString& filename)
{
........
........
}

void save(constQString& filename)
{
........
........
}
private:
QProject(){} // блокируем конструктор
QStringList m_filesList;
};

Уже любопытно не правда ли? Экземпляр класса QProject невозможно создать никаким образом! Далее пронаследуемся от QApplication:

class QMyApplication : public QApplication
{
public:
QMyApplication(int argc, char *argv[]) :
QApplication(argc, argv)
{}
void createProject();
void closeProject();
QProject *project() const;
private:
QProject m_project; // указатель на проект
};

Пока вроде ничего необычного. А теперь собственно трюк:

class QMyApplication;

class QProject // класс проекта
{
public:
Q_DISABLE_COPY(QProject) // блокируем копирование объекта

// добавить файл в проект
void appendFile(const QString& filename)
{
if (!m_filesList.contains(filename))
m_filesList.append(filename)
}
// удалить файл из проекта
void removeFile(const QString& filename)
{
int index = m_filesList.indexOf(filename));
if (index != -1)
m_filesList.remove(index);
}

private:
QProject(){} // блокируем конструктор
QStringList m_filesList;
friend class QMyApplication;
};

class QMyApplication : public QApplication
{
public:
QMyApplication(int argc, char *argv[]) :
QApplication(argc, argv),
m_project(NULL),
refCount(0)
{
}
void createProject()
{
if (refCount < 1)
{
m_project = new QProject();
}
}
void closeProject()
{
if (refCount > 0)
{
delete m_project;
m_project = NULL;
}
}
QProject *project() const { return m_project; }
private:
int refCount; // счетчик ссылок
QProject m_project; // указатель на проект
};


Что получается: QProject не имеет возможности создать себя сам, однако экземпляр можно создать только через QMyApplication, который по умолчанию Singltone! Таким образом можно гарантировать, что во время работы программы будет создан лишь один единственный экземпляр QProject! Более того, переписав макрос qApp можно получать доступ к проекту из любой точки программы!