Очень простая сериализация для C++

ОГЛАВЛЕНИЕ

В данной статье описана реализация легкого механизма для сохранения объектов C++ в XML или двоичные форматы.

•    Скачать исходники с файлами проекта для VS2003 - 31.6 Кб
•    Скачать исходники с файлами проекта для VS2008 - 31.6 Кб
•    Скачать исходники и двоичный файл выпуска - 313 Кб (содержит демонстрационный проект GUI(графический пользовательский интерфейс) для VS2003)

Введение

В данной статье описана реализация легкого механизма для сохранения объектов C++ в XML или двоичные форматы. Статьи такого рода обычно не порождают много графического содержимого, отсюда много вставленных фрагментов кода.

Пример кода содержит проекты VS2003 и VS2008, формирующие консольное приложение для блочного теста. /W4 используется везде.

Кроме того, есть очень простое приложение MFC Контакты, отображающее содержимое в гибридном элементе управления сетка-дерево. Задача приложения GUI – показать, как:
•    Расширить маршалирование очень простой сериализации для ваших собственных классов
•    Использовать простой контроль версий
•    Очень простая сериализация справляется с реальным кодом – защищенными конструкторами, виртуальными пустыми функциями, и т.д.
•    Создание динамических объектов – и как очень простая сериализация обрабатывает ошибки

Когда использовать

У данной методики есть много применений, включая сохранение настроек программы, сохранение состояния для операций отмены/восстановления, автоматической активации форматов файла XML для данных приложения, связи клиент-сервер (т.е. пакеты в проводе) и хранения нереляционных данных в виде XML в базах данных SQL.

Основные особенности

1.    Весь код соответствует ISO C++ и переносим
2.    Не требует, чтобы постоянные классы имели общий базовый класс
3.    Не требует компиляции с поддержкой информации о типе времени исполнения
4.    Соблюдает существующий контроль доступа для конструкторов и деструкторов
5.    Сериализует указатели на классы/структуры, поддающиеся сериализации
6.    Правильно восстанавливает содержимое контейнеров указателей на полиморфные объекты
7.    Делает упор на проверку во время компиляции, чтобы минимизировать ошибки при выполнении
8.    Макросы используются только для краткости и направляются в отлаживаемый код
9.    Реализация полностью встроенная – надо только включить заголовочные файлы очень простой сериализации посредством #include
10.    Очень просто добавить новые форматы хранения - JSON, к примеру

Ограничения

1.    Требует, чтобы сериализуемые классы имели пустой конструктор
2.    Текущая реализация предполагает, что сериализация производится в одном потоке – немного нужно, чтобы добавить безопасность потоков
3.    Явно не поддерживает сериализацию типов-указателей 'C', особенно void* и friend(дружественный)
4.    Пока не реализовано хранилище строки UNICODE для XML
5.    Нет теоретических препятствий к использованию очень простой сериализации с множественным наследованием, но вообще не тестировалось

Соглашения

Во избежание бесчисленных повторений принимается, что любой класс C0 является базовым классом в произвольной иерархии, где C1 унаследован от C0, а C2, в свою очередь, унаследован от C1. Корневой класс описывает «наименее производный» класс. RTTI - информация о типе времени исполнения, и это сокращение применяется при рассмотрении того, как хранить запись имен классов и информацию о наследовании во время выполнения. Следовательно, имеется:

Замечание о макросах и шаблонах

Вкратце, обычно макросы очень полезны при применении с толком. При этом предпочтителен код, поддающийся правильной отладке и предоставляющий возможность легкого тестирования для каждого макроса, а именно - виден ли код или текст во время вхождения в макрос в отладчике?

Макросы ESS_REGISTER, ESS_RTTI и ESS_STREAM используют оператор преобразования в строку (#) для порождения строк из имен классов и экземпляров. Это хорошо, так как уменьшает возможность для ошибки. ESS_RTTI также объявляет дружественный шаблонный класс фабрики, отвечающий за создание новых экземпляров в куче, следовательно, у него есть доступ к защищенным/закрытым конструкторам и деструкторам. Это значительно упрощает применение очень простой сериализации к существующему коду, принося решительную пользу.

Если в макросе содержится исполняемый код, он всегда направляется в шаблонную встроенную функцию для обеспечения правильной отладки. Например:

// тривиальный
#define ESS_ROOT(rootname) typedef ess::root<rootname> ess_root;

// направляется в шаблонную функцию
#define ESS_STREAM(stream_adapter,class_member)        \
    ess::stream(stream_adapter,class_member,#class_member)

// и чуть более сложная комбинация ...
#define ESS_RTTI(classname,rootname)\
friend ess::CFactory<classname,rootname>; \
virtual const char* get_name()\
{ return ess::get_name_impl<classname>(#classname); }\
static ess::class_registry<classname>* get_registry()\
{ return ess::get_registry_root<classname,rootname>(#rootname); }

ESS_RTTI – самый сложный из макросов очень простой сериализации.

Еще одно философское заявление: шаблоны прекрасны, но метапрограммирование шаблона – нет. Почему? Метапрограммирование шаблона не выдерживает тест отладчика.


Пример очень простой сериализации

Начнем с простого примера. C0 – корневой класс, подлежащий сериализации. Ниже приведена очень простая встроенная реализация.

// главный заголовочный файл
#include "ess_stream.h"
// используйте этот заголовок для хранилища XML
#include "ess_xml.h"
class C0
{
    // следовательно, можно отличить
    short m_id;
    // вектор указателей на C0
    std::vector<C0*> m_children;
    // здесь функция сериализации -
    // она симметрична, работает для чтения и записи
    virtual void serialize(ess::archive_adapter& adapter)
    {
        ESS_STREAM(adapter,m_id);
        ESS_STREAM(adapter,m_children);
    }
public:
    // задается корень наследования
    ESS_ROOT(C0)
    // устанавливается RTTI
    ESS_RTTI(C0,C0)
};

class C1: public C0
{
    // для примера – настоящий класс,
    // вероятно, имел бы больше кода
    ESS_RTTI(C1,C0)
};

ниже приведен код для выполнения сериализации в обоих направлениях:

int version = 1;
std::string xml_root = "root";
std::string instance_name = "x";
// всегда используйте блоки попытка-перехват, так как любые проблемы используют throw()
try
{
    // регистрируется класс
    ess::Registry registry;
    // разновидность с макросом для краткости, орфографических ошибок нет
    registry << ESS_REGISTER(C0,C0);

    // где хранятся данные...
    ess::xml_medium storage;
    {
        // экземпляр для сериализации
        C0 c0;
        C1 c1;
        // эта версия скрывает парсер XML...
        ess::xml_storing_adapter adapter(the_storage,xml_root,version);
        // сохранить корневой C0
        ess::stream(adapter,c0,"c0");
        // сохранить производный C1
        ess::stream(adapter,c1,"c1");
    }
    // десериализовать в p0
    {
        //
        C0* p0 = 0;
        // восстановить из хранилища XML
        Chordia::xml_source xmls(storage.c_str(),storage.size());
        // и адаптер
        ess::xml_loading_adapter adapter(xmls,xml_root,version);
        // направить в указатель C0...
        ess::stream(adapter,p0,instance_name);
        // теперь p0 готов к использованию ...
        delete p0;
    }
}
catch(...)
{
}

XML, сгенерированный в example(), выглядит так:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<root version="1"/>
<class derived_type="C0" name="c0">
    <signed_short name="m_id" value="1"/>
    <vector name="m_children" count="0">
    </vector>
</class>
<class derived_type="C1" name="c1">
    <signed_short name="m_id" value="1"/>
    <vector name="m_children" count="0">
    </vector>
</class>
</root>

Подробно

•    RTTI(информация о типе времени исполнения)
•    Регистрация
•    Адаптеры
•    Обработка ошибок
•    Блочные тесты

Перейдем к деталям. Сохранить базовые классы C++ не слишком трудно - MFC испокон веков имел механизм для осуществления этого. Начнем аналогичным образом; основой системы является сериализация путем разбиения – классы сводятся к мельчайшим элементам, которые затем записываются и считываются во время выполнения. Чтение и запись использует симметричную функцию serialize(), уменьшающую требования к программированию и возможные ошибки. По-настоящему хитрые куски исходят из следующего:

1.    Нет общего базового класса
2.    Правильное восстановление указателей на экземпляры полиморфных классов
3.    Обеспечение удобства функции для компилятора
4.    Обеспечение удобства функции для программиста


RTTI и дружественность

Допущение 1: Чтобы правильно восстанавливать полиморфные типы, надо:

1.    иметь возможность некоторым образом различать производные типы, и
2.    иметь возможность делать это во время выполнения.

Перейдем  к сути: простейший способ сделать это – снабдить каждый класс, совместимый с очень простой сериализацией, виртуальной функцией get_name(). Затем можно сделать следующее:

// идентификация экземпляров во время выполнения
std::vector<C0*> vec;
vec.push_back(new C0);    // базовый класс
vec.push_back(new C1);    // производный класс
std::string n0 = vec[0]->get_name(); // дает "C0"
std::string n1 = vec[1]->get_name(); // дает "C1"

Допущение 1 значит, что надо иметь возможность создавать (безопасно для типов) произвольные экземпляры разных типов, если дана только строка. Также есть добавленное новшество, показанное тут путем введения независимой иерархии с префиксом 'D':

// Пример 1.1
// не C++ ?
C0* pc0 = hey_presto("C0");
C1* pc1 = hey_presto("C1");
D0* pd0 = hey_presto("D0");
D1* pd1 = hey_presto("D1");

Нечто близкое к примеру 1.1 можно получить путем добавления такой статической функции в каждый базовый класс C0 и D0, что:

C0* pc0 = C0::hey_presto("C0");
D0* pd0 = D0::hey_presto("D0");

Возьмем шаблоны. Не придется указывать имя типа, если будет иметься шаблонная версия hey_presto():

template <typename T>
inline
T*
hey_presto(const std::string& classname)
{
    // найти имя класса в чем-то
    return new T_Or_Derivative_Of_T;
}

// т.е.
C0 pc0 = hey_presto<C0>>("C0");
D0 pd0 = hey_presto<D0>("C0");

На самом деле решение более сложное. Общее решение косвенности применяется для обеспечения безопасности типов, гибкости и эффективности. Добавляется еще один макрос – он уже был представлен, но рассмотрим его внимательнее:

// упрощается для примера путем удаления
// описателя пространства имен ess::
static class_registry<classname>* get_registry()
{
    return get_registry_root<classname,rootname>(#rootname);
}

// следовательно, вызов макроса ESS_RTTI(C0,C0) становится:
class C0
{
    // статическая функция возвращает шаблонный тип
    static class_registry<C0>* get_registry()
    {
        return get_registry_root<C0,C0>("C0");
    }
};

Если продолжить следовать пути вызова, получится следующее подряд:

//-----------------------------------------------------------------------------
// Фрагмент 1:
// Упрощенный get_registry_root
template <typename Derived,typename Root>
inline
class_registry<Derived>* get_registry_root(const char* rootname)
{
    return
        reinterpret_cast<class_registry<Derived>*>
            (get_registry_impl<Root>(rootname));
}

//-----------------------------------------------------------------------------
// Фрагмент 2:
// шаблонная встроенная функция, вызываемая реализацией макроса ESS_ROOT
template <typename Root>
inline
class_registry<Root>* get_registry_impl(const char* rootname)
{
    // при вызове этой функции создается реестр для
    // иерархии на базе T и существует
    // на протяжении выполнения программы.
    static ess::class_registry<Root> s_registry(rootname);
    return &s_registry;
}

//-----------------------------------------------------------------------------
// Фрагмент 3:
// Наконец достигли нижнего этажа! Здесь опущены детали.
template <typename Root>
class class_registry
{
    public:
    // регистрируется фабрика, умеющая создавать корневой класс
    bool Register(const char* classname,IFactory<Root>* pFactory) {}
    // точка создания экземпляров, производных от корневого класса
    Root* Create(const std::string& classname) {}
};

Код в фрагментах выше снабжает каждый корневой класс статическим шаблонным экземпляром class_registry. Как подсказывают имена функций-членов, class_registry<C0>->Create("C0") действительно вернет новый экземпляр C0. Более детально функция-член Register() будет рассмотрена в следующем разделе – приближаемся к нужной ранее функции hey_presto().

Как всегда с C++, дьявол кроется в деталях. get_registry_impl() в фрагменте 2 выше возвращает указатель на экземпляр статического класса. Стало быть::
1.    всегда будет существовать только один экземпляр class_registry
2.    class_registry будет создан только при вызове get_registry_impl()
3.    class_registry доступен всем классам, производным от Root

Это, в свою очередь, значит, что возможно следующее:

// дает C0
C0* p0 = C0::get_registry()->Create("C0");
// дает производный C1, но доступный только через корневой тип
C0* p0 = C0::get_registry()->Create("C1");

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

// да – как ожидалось
C0* p0 = C0::get_registry()->Create("C0");
// вуаля!
C1* p1 = C1::get_registry()->Create("C1");

Хотя это выглядит достаточно просто, вспомните, что статические функции C++ не виртуальные. Не может быть статических функций с одинаковым именем в двух разных, но родственных классах. Или может? Посмотрим снова:

// да – как ожидалось
class_registry<C0>* rc0 = C0::get_registry();
C0* p0 = rc0->Create("C0");
// вуаля!
class_registry<C1>* rc1 = C1::get_registry();
C1* p1 = rc1->Create("C1");

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

template<typename Type>
inline
Type*
instance_from_name(const std::string& classname)
{
    // так как get registry – статическая с иной сигнатурой,
    // на каждом уровне наследования можно переопределить имя функции
    ess::class_registry<Type>* p = Type::get_registry();
    // создает правильный производный тип или выбрасывает ...
    return p->Create(classname);
}

Теперь имеется одна функция, работающая в обоих случаях, - заметьте, что тип аргумента шаблона отличается в (3).

// 1. получить корневой из корневого
C0* p0 = instance_from_name<C0>("C0");
// 2. получить производный через корневой – хорошо для перегрузки контейнеров
C1* p1a = instance_from_name<C0>("C1");
// 3. теперь можно получить производный из производного, следовательно,
// можно прямо обращаться к функциям-членам C1
C1* p1b = instance_from_name<C1>("C1");

Чтобы завершить этот несколько сложный раздел, проследуем за компилятором, когда данные направляются обратно из хранилища. Здесь важная встроенная функция в ess_stream.h; это шаблонная функция с сигнатурой, соответствующей указателям на типы:

template<typename Type>
inline
void
stream(stream_adapter& adapter,Type*& pointer,const std::string& name)
{
    std::string derived_type = get_class_name(adapter);
    // упрощенный
    pointer = instance_from_name<Type>(derived_type);
    //arg = instance_from_name(derived_type);
    // десериализовать экземпляр
    pointer->serialize(adapter);
}

// пример использования
C0* p0 = 0;
ess_stream(...,p0,...);
C1* p1 = 0;
ess_stream(...,p1,...);

В приведенном коде замечательно то, что весь он возвращает одно и то же –  статический класс реестра, давно объявленный в корне C0. Шаблонизация означает, что компилятор может несколькими способами установить безопасный, с точки зрения типов, способ обращения к реестру, позволяя создавать экземпляры произвольного типа. Однако это удобство обходится высокой ценой. Теперь теоретически возможно создавать экземпляры частично законченных классов! Без использования RTTI, генерируемой компилятором, невозможно защититься от этой ошибки во время компиляции. Очень тяжело защититься от нее во время выполнения.

// патологический
C1* p1 = instance_from_name<C1>("C0");

Сокращение регистрации

Назначение регистрации – следить, чтобы реестр каждого класса создавался перед любой попыткой создания. Хороший побочный эффект реализации заключается в том, что весьма трудно сделать это – как-никак, любой код, сериализующий класс, обращается к реестру. Однако при открытии нового постоянного объектного приложения с поддержкой XML и выборе «Файл >>: Открыть» среда выполнения начнет выбрасывать исключения при попытке создать экземпляры классов, еще не добавленных в систему. Явная регистрация полезна, так как она облегчает выяснение, где запускается постоянный процесс, тем самым упрощая отладку или диагностику ошибок. Сама регистрация простая и производится лишь однажды.

// использовать обычное письмо
ess::registry_manager registry;
    registry
        << ess::class_registrar<C0,C0>("C0")
        << ess::class_registrar<C1,C0>("C1")
        << ess::class_registrar<C2,C0>("C2");
// макрос-сокращение
ess::registry_manager registry;
    registry
        << ESS_REGISTER(C0,C0)
        << ESS_REGISTER(C1,C0)
        << ESS_REGISTER(C2,C0);

Заметьте, что объект реестра не обязательно хранить. Регистрация делает три вещи:

•    создает статический экземпляр class_registry,
•    создает шаблонный класс фабрики для создания данного типа,
•    вставляет экземпляр класса фабрики в реестр под ключом имени класса.

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

Есть другая разновидность registry_manager, полезная при работе с Редактором диаграмм, размещенным на CodeProject несколько лет назад. Имеется сильно измененная версия, использующая очень простую сериализацию для отмены/восстановления и сохранения в виде XML.

// типизированный реестр с корнем CDiagramEntity
ess::typed_registry_manager<CDiagramEntity> registry;
// регистрирует 3 нужных класса
registry
    << ESS_REGISTER(CEditor,CDiagramEntity)
    << ESS_REGISTER(CListBox,CDiagramEntity)
    << ESS_REGISTER(CStatic,CDiagramEntity);

// typed_registry_manager предоставляет instance_from_name()

Адаптеры

Назначение archive_adapter – облегчить сохранение в новых форматах. Класс адаптера, загружающий из, скажем, JSON или некоторого закрытого двоичного формата, должен лишь реализовывать перегруженные функции read() в классе archive_adapter. То же самое справедливо для пишущего адаптера. Исходный код показывает совершенно разный подход к адаптеру – посмотрите на класс binary_debug_adapter в ess_binary.h. Он загружает двоичный архив в текстовый файл по мере его передачи в реальном времени; помогает более подробно понять двоичное хранилище.

XML – предпочтительный формат хранения – желательно, чтобы сгенерированный XML содержал достаточно информации, чтобы позволить отладку программы по исходному тексту, и повсеместность формата значит простоту обмена и совместимости. Наряду с хранением содержимого членов класса, надо хранить их имена. Для каждого из встроенных типов, вместе с поддерживаемыми типами контейнера, есть несколько встроенных свободных функций, именуемых stream, имеющих конкретную сигнатуру типа и делающих одно и то же – принимающих параметры arg и name, а затем:

1.    Если archive_adapter сохраняет, записывает данные аргумента и его имя в нижележащее хранилище
2.    Если archive_adapter загружает, то считывает значение именованного элемента обратно в параметр arg

namespace ess
{
// для каждого встроенного
inline void
    stream(archive_adapter& adapter,bool& arg,const std::string& name)    {...}
// ... дополнительные свободные функции, как выше
inline void
    stream(archive_adapter& adapter,GUID& arg,const std::string& name)    {...}
// теперь имеется обобщенная шаблонизация. Следующее дано для информации
template<typename Type> inline void
    stream(archive_adapter& adapter,Type& arg,const std::string& name) {...}
// шаблон для типов указателей
template<typename Type> inline void
stream(archive_adapter& adapter,Type*& arg,const std::string& name)    {...}
// специализации для std::vector
template<class Type> inline void
    stream(archive_adapter& adapter,std::vector<Type>& arg,const std::string& name)    {...}
// и для std::map
template<typename Key,typename Value> inline void
    stream(archive_adapter& adapter,std::map<Key,Value>& arg,const std::string& name) {...}
}

Обнаружение ошибок

Хотя очень простая сериализация облегчает добавление сохранения при выполнении в классы C++, нежелательно жертвовать временем компиляции, отведенным на проверку языка. Действительно, по возможности, надо предупредить программиста о наличии ошибки. Рассмотрим следующее:

ESS_ROOT(C0)
ESS_RTTI(C0,C0)
ESS_RTTI(C1,C0)
ESS_RTTI(C2,C1) <- неверно...

все же легко сделано. C1 – не корневой класс. Как можно определить это во время компиляции? С некоторым трудом! Есть нечто по имени compile_time_checker в заголовочном файле ess_rtti. Оно гарантирует, что класс, объявленный как ESS_ROOT, всегда используется как корневой в макросе ESS_RTTI. То есть он не будет компилироваться, если возникнет такая ошибка, как показано выше.

template <typename Derived,typename Root>
struct compile_time_checker

Следующие категории ошибок не требуют вспомогательного кода, поскольку являются синтаксическими ошибками или (более вероятно) неразрешимыми, несвязанными ошибками типа.

•    Попытка сериализовать неподдерживаемый тип
•    Добавление сохраняемости к типу с непустыми конструкторами типа
•    Попытка сериализовать класс или структуру, не реализующую get_name()
•    Попытка сериализовать класс или структуру, не реализующую get_registry()
•    Попытка сериализовать класс или структуру, не реализующую serialize() где-либо в иерархии
•    Несуществующие порождения

class CD3 : public CD2 { ESS_RTTI(CD3,CDX) }

Суть в том, что предсказуемые ошибки при выполнении таковы:

•    Попытка сериализовать незарегистрированный тип - т.е. загрузка класса, неизвестного компилятору. Это не удастся, так как экземпляр class_registry выбросит исключение.
•    Попытка десериализовать экземпляр, устройство которого как-то изменилось. Манера отказа этого режима важна – если устройство не в порядке, то среда выполнения должна обнаружить это и выбросить исключение с помощью throw().

Блочные тесты

Они весьма простые и все содержатся в исходном файле ess_main.cpp. Идея – собрать контрольный пример, проверяющий (или нет) ключевые требования к реализации. Чтобы код компилировался, он должен отвечать базовому набору требований, - или проверяется столько ошибок, сколько можно проверить во время компиляции. Однако ряд условий можно проверить только при выполнении. Самые простые тесты таковы:
1.    Сохранит ли постоянный класс сам себя?
2.    Будет ли данных, сгенерированных путем сохранения класса, достаточно для создания нового экземпляра?
3.    Если новый экземпляр сам сериализован, будет ли полученное хранилище равняться исходному хранилищу (т.е. из 1.)?
4.    Может ли среда выполнения обеспечить обнаружение ошибок программирования, таких как неправильное порождение?

Настройка очень простой сериализации: 42-строчное руководство

Здесь приведены шаги, требуемые для обеспечения совместимости любого класса с очень простой сериализацией. Весь код содержится в пространстве имен ess, отсюда и описатель ess:: везде.

// главный включаемый файл очень простой сериализации - добавляет ess_rtti.h
#include "ess_stream.h"
// Для хранилища XML.
#include "ess_xml.h"
// или для двоичного хранилища
#include "ess_binary.h"

// базовый класс любой постоянной иерархии использует макрос ESS_ROOT
class persistent_base
{
    // пример постоянного члена
    some_type class_member;
    public:
    // ESS_ROOT используется в «наименее производном» классе
    ESS_ROOT(persistent_base)
    ESS_RTTI(persistent_base,persistent_base)
    // функция сериализации - виртуальная
    virtual void serialize(ess::archive_adapter& adapter)
    {
        //
        ESS_STREAM(adapter,class_member);
    }
}

// любые последующие потомки используют макрос ESS_RTTI
class persistent_derived : public persistent_base
{
    some_type class_member;
    public:
    // обратите внимание на аргументы ESS_RTTI – имя этого класса,
    // затем имя корневого класса
    ESS_RTTI(persistent_derived,persistent_base)
    // убедиться, что вызывается serialize в базовом классе ...
    virtual void serialize(ess::archive_adapter& adapter)
    {
        // передаются члены этого класса
        ESS_STREAM(adapter,class_member);
        // передаются члены базового класса
        persistent_base::serialize(adapter);
    }
}

Вся реализация встроенная, и за исключением CoCreateGuid(),система поддержки исполнения программы использует только конструкции в пространстве имен std::, а именно std::string, std::map и std::vector. Если вам нужны более подробные сведения о том, как расширить очень простую сериализацию, чтобы упорядочивать ваши собственные типы, то смотрите файл ess_class.h в проекте ESS_GUI. Он показывает, как сохранить COleDateTime.

Исходный код

Архивы VS2003 и VS2008 содержат две папки:

•    ./codeproject/ess_code/ess_0X
•    ./include/...

Убедитесь, что пути созданы при распаковке, так как это значит, что проекты должны компоноваться по умолчанию, без необходимости устанавливать новые пути #include и тому подобное. Проект MFC GUI должен распаковаться точно так же.

Пункты на будущее

В текущей реализации сознательно опущены следующие вопросы.

•    Поддержка для реже используемых контейнеров, например, std::list and std::stack. Приведенный код редко использует эти классы – поддержку легко добавить.
•    Интеллектуальные указатели и дружественность – недавно появились в обновлении TR для VS2008 и пока отсутствуют в стандартных библиотеках C++. Не было желания делать собственную библиотеку.
•    Проблемы с порядком байтов в двоичной системе хранения – сейчас используется порядок Intel. Было бы хорошо использовать сетевой порядок для двоичного хранилища.

Другие возникающие вопросы

•    Есть неприятная антисимметрия в версиях хранилища XML для чтения и для записи. Хотелось бы сгладить ее.
•    Эффективность. Вероятно, верхние уровни считывателя/писателя XML выдержат некоторое упрощение.

Заключение

На этом все. Было убедительно показано, что безопасный с точки зрения типов, соответствующий стандартам и переносимый постоянный код C++ может быть создан за счет минимума работ по программированию. Интересно сравнение с кодом C#. Отказ от менее эффективного, но автоматизированного сохранения, предусмотренного отражением, ручное задание членов для сериализации с помощью тега XML дает уровень избытка кода, сходный с очень простой сериализацией.

Не проверялась компиляция с помощью Visual C++ 6.0, поскольку он больше не используется. В нем нет механизма шаблонизации, необходимого для работы очень простой сериализации. Также, несмотря на все усилия, не удалось убедить компилятор C, прилагаемый к Cygwin, найти правильные стандартные заголовочные файлы.