Очень простая сериализация для C++ - Адаптеры, обработка ошибок и блочные тесты

ОГЛАВЛЕНИЕ

Адаптеры

Назначение 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, найти правильные стандартные заголовочные файлы.