Очень простая сериализация для C++ - RTTI и дружественность

ОГЛАВЛЕНИЕ

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()