MFC – Множественное наследование и сериализация - Реализация Serialize

ОГЛАВЛЕНИЕ

Реализация Serialize

Сериализация CObject и ESerializable не конфликтуют: может иметься класс, унаследованный от CObject и ETypeSerilizable (многократно), но избегайте реализации DECLARE/IMPLEMENT_SERIAL и экземпляра ETypeSerializable, потому что нельзя – для одного и того же объекта – сохранять его иногда одним образом, а иногда другим: карты, следящие за идентичностью объектов, различаются (встроены в CArchive для CObject* и реализованы с помощью SArchiveMaps для ESerializable*), следовательно, есть риск сохранить объект дважды и при загрузке загрузить две отдельные идентичные копии.

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

Допустим, класс C унаследован от A и B, оба из них унаследованы от D и виртуально от E. Допустим, все эти классы также могут существовать как независимые классы и все являются сериализуемыми (все они унаследованы от их соответствующего класса ETypeSerializable<>).

На следующем рисунке показана иерархическая схема.

 

Есть два экземпляра D и лишь один экземпляр E (но на него ссылаются дважды). Это ведет к двум проблемам.

Первое: избегайте сериализации уже сериализованных компонентов

Допустим, C::Serialize реализуется путем вызова A::Serialize и B::Serialize.

Допустим, A::Serialize вызывает D::Serialize и E::Serialize. И B::Serialize вызывает D::Serilize и E::Serilize.

Сериализуйте C, и E сериализуется дважды. То есть , вероятно, потребуется  проверить перед сериализацией чего-то, было ли это уже сериализовано. Но проблема не заключена в циклических указателях: на E не ссылаются A и B с помощью указателя.

Конечно, можно не вызывать E::Serialize из B, но что произойдет, если B существует сам по себе (не как базовый класс для C)? Его компонент E будет отсутствовать.

Для решения этой проблемы в SArchiveMaps была реализована карта,  ключ которой  формируется парой указателей: один –  для SSerializWatchDoc, а второй – для ESerializable. Это значение - UINT, что на самом деле представляет собой счетчик.

SSerilizeMaps, который уже предоставляет функцию MapObject (она вызывается с помощью “Load” и “Save”, но можно вызывать ее в случаях, аналогично требующих вызова CArhive::MapObject для производного CObject), также предоставляет MapWatchDog, принимающий как аргументы SSerializeWatchDog и ESerializable.

SSerializeWatchDog, в свою очередь, предоставляет только одну функцию, а именно:

bool Locked(CArchive& ar, LPVOID pInstance).

Эта функция находит карты, связанные с архивом, и вызывает MapWatchDog, передавая себя заданный LPVOID. MapWatchDog ищет (и выделяет) запись и увеличивает счетчик. Locked возвращает true, если возвращенный счетчик больше 1.

Какую бы составляющую C, являющегося порождением от виртуального E, вы бы ни привели к E*, вы всегда будете получать одно и то же значение указателя (указатель "this" для E одинаков, но есть два разных указателя "this" для D)

Многократной сериализации базовых классов можно избежать путем создания статической переменной SSerializeWatchDog в начале функции Serilize и немедленного возврата управления, если Locked(ar, this) вернул true: это значит, что это тело (представленное переменной-сторожем) уже было выполнено над тем экземпляром (представленным this, приведенным к LPVOID)

Все структуры данных удаляются при уничтожении SArchiveMaps. Ниже приведены примеры сериализаций C, B и E

void C::Serialize(CArchive& ar)
{
    static SSerializeWatchDog wd;
    if(wd.Locked(ar, this)) return;
   
    A::Serialize(ar);
    B::Serialize(ar);
   
    if(ar.IsStoring())
        //сохранить члены C
    else
        //загрузить члены C
}          

void B::Serialize(BArchive& ar)
{
    static SSerializeWatchDog wd;
    if(wd.Locked(ar, this)) return;
   
    E::Serialize(ar);
    D::Serialize(ar);
   
    if(ar.IsStoring())
        //сохранить члены B
    else
        //загрузить члены B
}          

void E::Serialize(BArchive& ar)
{
    static SSerializeWatchDog wd;
    if(wd.Locked(ar, this)) return;

    if(ar.IsStoring())
        //сохранить члены E
    else
        //загрузить члены E
}        

Макрос GE_SERIALIZE_CHECK_MULTIPLE() реализует первые строки кода функций сериализации.

Второе: различайте компонент одного и того же типа

Допустим, есть два D*, указывающих соответственно на два разных компонента D одного и того же объекта C (объект C на предыдущем рисунке). При сохранении первого указателя весь C сериализуется, тогда как при сохранении второго должна сохраняться только метка карты.

Но при обратной загрузке нельзя загрузить C и затем преобразовать в D*, потому что такое преобразование будет неоднозначным. Поэтому метку карты нельзя генерировать только для целых объектов (как делает MFC), а надо генерировать для каждого компонента.

Решение проблемы – отметить на карте обобщенные LPVOID (два D* различаются) и создать метку для каждого во время сериализации C. Это легко делается с помощью SArchiveMaps::MapWatchDog: эта функция вызывается в теле Serialize, передавая указатель “this”: для решения проблемы вызывается MapObject(pInstance).

Следовательно, чтобы решить проблему нескольких базовых классов, всегда в каждом теле Serialize определяйте static SSrializeWatchDog и вызывайте Locked. Он отметит объект на карте и проверит, было ли тело уже выполнено для этого экземпляра. Если он вернет истину, сразу возвращайте управление из этого тела. Если хотите, используйте макрос GE_SERIALIZE_CHECK_MULTIPLE(CArchive). Он сам сделает эти операции.