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

ОГЛАВЛЕНИЕ

Данная статья предлагает некоторые замены для модели MFC для объявлений типов в пространствах имен, использования шаблонов для сериализации, безопасной сериализации с классами множественного наследования, и т.д.

•    Скачать исходники - 20.7 Кб
•    Скачать демонстрационный проект - 40.8 Кб
•    Скачать демонстрационный исполнимый файл - 142 Кб

Введение

MFC(библиотека базовых классов Microsoft) является прекрасной оберткой для W32 API в среде C++. Однако MFC почти “бедна” в плане реализации C++ из-за определенных недостатков (вероятно, унаследованных в результате совместимости с прошлыми версиями).

•    Первое - MFC ничего не знает о пространствах имен.
•    Второе - MFC включает в себя собственные коллекции и не считает STL(стандартная библиотека шаблонов) удобной библиотекой для этой цели
•    Третье - MFC не рассчитана на множественное наследование (некоторые программисты считают его неразумным, но используют его при наследовании формы “интерфейс”, являющейся не чем иным, как абстрактным классом или структурой)
•    Четвертое – MFC излишне использует макросы, затрудняя использование типов с составными именами (попробуйте объявить как DECLARE_SERIAL шаблон или класс в пространстве имен)

Данная статья предлагает некоторые замены для модели MFC для объявлений типов в пространствах имен, использования шаблонов для сериализации, безопасной сериализации с классами множественного наследования, и т.д.

Предпосылки

Для читателя важен опыт программирования на MFC и знание исходного кода MFC. Мы не обучаем MFC и не переделываем его. Просто хотим идти вперед, оставляя MFC задачи, с которыми она справляется.

В примерах используются пространства имен, STL и некоторые вспомогательные классы. Для некоторых из них были написаны отдельные статьи. Если вам не нужны детали, эти статьи знать необязательно.

Недостатки MFC

Mfc и RTTI

На заре MS C++ такие "странные вещи", как шаблоны и информация о типе времени исполнения(RTTI), не поддерживались языком. Поскольку MFC требовался механизм информации о типе времени исполнения, она начала реализовывать собственный.

Сейчас, даже при поддержке RTTI языком, этот механизм все еще существует по двум причинам. Первая - “совместимость с прошлыми версиями”, а вторая - “динамическое создание”: по сути, то, что делает MFC, - это не только RTTI, но и способ динамического создания полиморфных объектов.

Несмотря на наличие частичного совпадения, оба механизма имеют недостатки:

•    MFC DECLARE_SERIAL явно рассчитан на объекты одиночного наследования. Он основан на макросе, предварительно обрабатывающем имена типов (вставляется метка для превращения имени класса в имя статической переменной и в строку: например, это не работает с шаблонами или с составными именами, такими как вызовы в пространстве имен).
•    RTTI обеспечивает идентификацию и преобразование типов во время выполнения (dynamic_cast), но не может в одиночку произвести динамическое создание.

Хорошо, что два механизма не конфликтуют: включение RTTI позволяет использовать dynamic_cast для объекта одиночного и множественного наследования (механизм MFC вообще не знает о множественном наследовании), и порождение CObject с DECLARE/IMPLEMENT_SERIAL разрешает динамическое создание. Но управление сериализацией базового класса остается на ваше усмотрение.

Последний факт создает проблему, если базовый класс наследуется неоднократно или виртуально наследуется несколькими классами.

Следовательно, надо дополнить RTTI и MFC, чтобы они поддерживали динамическое создание и сериализацию объектов множественного наследования без макросов.

MFC и пространства имен

Все классы Mfc определены в глобальном пространстве имен.

Нелегко поместить их в пространство имен, отличное от глобального: код MFC изобилует подобными вызовами функций “::function(...)”: переместите эту функцию в пространство имен –  и окажетесь в тупике. Но даже если не трогать MFC и определить в пространствах имен только свои классы, по-прежнему возникают трудности: макросы MFC .DECLARE_SERIAL,DECLARE_MESSAGEMAP, и др. принимают имя класса как параметр. IMPLEMENT_SERIAL, BEGIN_MESSAGE_MAP и др. принимают имя класса и имя базового класса как параметры. Чтобы заставить все это работать, надо устроить пространства имен так, чтобы имена классов в макросах всегда были “простыми именами”.

Одно из возможных решений показано  здесь:

//  AppFrameWnd.h 
#pragma once
    namespace GE_{namespace App{
        class
        CAppFrameWnd : public CFrameWnd
        {
        public:
            CAppFrameWnd(void);
            ~CAppFrameWnd(void);
 DECLARE_MESSAGE_MAP()
            afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
        };

    }}
   
//AppframeWnd.cpp
#include "StdAfx.h"
#include "appframewnd.h"

namespace GE_{namespace App{
    BEGIN_MESSAGE_MAP(CAppFrameWnd, CFrameWnd)
        ON_WM_CREATE()
    END_MESSAGE_MAP()
   
    CAppFrameWnd::CAppFrameWnd(void)
    {   }
   
    CAppFrameWnd::~CAppFrameWnd(void)
    {    }
   
    int CAppFrameWnd::OnCreate(LPCREATESTRUCT lpCreateStruct)
    {
        if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1;

        return 0;
    }
}}

Этот код скомпилируется правильно. Но, берегитесь: все мастера склонны помещать весь сгенерированный код на глобальный уровень, указывая полное имя.

При создании карты сообщений (путем добавления обработчика WM_CREATE через окно свойств) код появляется  в таком виде:

//AppframeWnd.cpp
#include "StdAfx.h"
#include "appframewnd.h"

namespace GE_{namespace App{

    CAppFrameWnd::CAppFrameWnd(void)
    {
    }

    CAppFrameWnd::~CAppFrameWnd(void)
    {
    }
}}

BEGIN_MESSAGE_MAP(GE_::App::CAppFrameWnd, CFrameWnd)
    ON_WM_CREATE()
END_MESSAGE_MAP()

int GE_::App::CAppFrameWnd::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if(CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1;

    return 0;
}

и при компиляции дает ошибку C2327 на GE_::App::CAppFrameWnd::OnCreate

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

Mfc и сериализация

Есть еще одна проблема –  сериализация. Представьте два класса с одним и тем же именем в разных пространствах имен – оба с DECLARE/IMPLEMENT_SERIAL.

Оба класса передадут одно и то же простое имя макросу, но классы и статические члены идут в разные пространства имен. Компилятор работает правильно, и код выполняется верно. Но при сериализации указателя на один из этих классов возникает проблема: при сериализации указателя на объект, если объект еще не был сериализован, MFC сохраняет в файл имя типа объекта, чтобы знать во время загрузки, какой CRuntimeClass использовать для вызова CreateObject(имя типа используется как ключ).

Но “имя” является тем, что написано в параметрах макроса! В системе есть два CRuntimeClass с одним и тем же m_lpszClassName. Следовательно, при загрузке MFC может создать объекты, перепутав их типы.


Предлагаемое решение

Находится в “Factory.h” и “Factory.cpp”. Набор шаблонных классов на базе RTTI для управления сериализацией вместо макросов.

Что такое SFactory?

Это замена CRuntimeClass. Это структура, содержащая const type_info* (берется из <typeinfo.h>, RTTI должен быть включен) и используемая для предоставления имени типа в виде строки.

SFactory привязывается к внутреннему std::list при создании и уничтожении и находится с помощью строки имени типа с использованием статической функции FindFactory. Он также определяет абстрактную функцию-член NewObject, но не реализует ее.

Полная функциональность реализована в шаблонном классе, унаследованном от SFactory, STypeFactory<E>. Его конструктор инициализирует type_info базового класса, чтобы он указывал на typeid(E), и NewObject переопределяется, чтобы он возвращал new E.

Обычно никогда не работают напрямую с этими классами. Между тем с ними взаимодействуют через другие классы: ESerializable и ETypeSerializable<E>.

ESerializable – класс, являющийся виртуально наследуемым от любого класса, который надо сделать сериализуемым (виртуальный, потому что ваш класс может иметь несколько базовых классов, каждый из которых сам может быть сериализуемым: но нужен только один экземпляр ESerializable на каждый объект).

Он определяет абстрактные функции: virtual void Serialize(CArchive& ar) и виртуальная SFactory* GetFactory(), по умолчанию возвращающая значение переменной члена.

Обычно об этом классе не заботятся. Заботятся о производном от него шаблоне: ETypeSerializable<E>.

Он виртуально наследует ESerializable и связывает STypeFactory<E> с типом E, устанавливая переменную члена его базового класса. GetFactory возвращает STypeFactory<E>.

Сейчас:

•    Чтобы сделать класс сериализуемым, надо унаследовать этот класс от ETypeSerilizable<yourclass> как последнего базового класса: это заставляет GetFactory() возвращать правильное значение (если это класс MFC, пусть первым базовым классом будет базовый класс MFC). Это равнозначно DECLARE_SERIAL

•    Чтобы сделать класс загружаемым, надо объявить на глобальном уровне (внутри безымянного пространства имен) переменную типа ETypeSerilizable<yourclass>. Ведь чтобы механизм загрузки работал, должна существовать хотя бы одна фабрика для вашего типа. Даже если еще не было загружено или создано ни одного объекта yourclass. Это равнозначно IMPLEMENT_SERIAL

Пример ниже:

// В файле заголовка (.H)
class YourClass: public YourBase, public ETypeSerializable<YourClass>
//Замечание: должен
//    быть
//    последний базовый класс
{
    // члены вашего класса
};

// в файле исходного кода (.CPP)
namespace {
   
    ETypeSerializable<YourClass> g_yourclassfactory;
   
}

Если надо сериализовать указатель –  вызывается SavePtr (при сохранении) и LoadPtr (при загрузке) с передачей переменной-указателя.

Замечание: Не были определены такие операторы, как << и >>, потому что они конфликтуют с оператором сохранения и загрузки указателя MFC.

Так как используются шаблоны, а не макросы, не будет никаких проблем с составными именами, такими как имена в пространствах имен или шаблонные классы.

Однако, чтобы сделать шаблонные классы сериализуемыми, нужен ETypeSerilizable<yourtemplateclass<yourparameters> > для каждого создания экземпляра шаблона, используемого в программе.

Например, если заголовок таков

template<class T>
class YourTemplate:
    public ETypeSerializable<YourTemplate<T> >
{
    // члены
    // функции (включая Serialize)
};

и вы собираетесь использовать YourTemplate для UINT и CString, то надо в файле cpp на глобальном уровне создать следующее

namespace { //безымянное пространство имен
   
    ETypeSerializable<YourTemplate<UINT> > g_yourtemplateForUint;
    ETypeSerializable<YourTemplate<CString> > g_yourtemplateForCString;

}              

Карты сериализации

При сохранении указателей, чтобы избежать многократной сериализации одного и того же объекта и отслеживания зацикливания, пришлось связать с архивом сериализации некоторые карты, связав метку с объектом (и наоборот).

Именно это (более или менее) делает MFC в своей реализации CArchive, но эти карты предназначены для производного CObject, а не для виртуально унаследованного ESerializable.

SArchiveMaps реализует то, что требуется. Должен быть создан его экземпляр, связанный с CArchive, перед началом сериализации/десериализации элементов ESerializable и уничтожен после завершения.

Более простой способ сделать это в приложении MFC – внутри производной реализации CDocument::Serialize: документы, фреймы и виды всегда создаются MFC через CDocTemplate и CRuntimeClass. При сохранении и загрузке CDocument::Serialize всегда вызывается. Итак ...

void CMyDocument::Serialize(CArchive& ar)
{
    GE_::Safe::SArchiveMaps maps(ar); //будет существовать до возврата
    if(ar.IsStoring())
    {
        ar << _valueobject;
        ar << _pCobjectDerived;
        GE_::Safe::SavePtr(ar, _pESerializableDerived);
    }
    else
    {
        ar >> _valueobject;
        ar >> _pCobjectDerived;
        GE_::Safe::LoadPtr(ar, _pESerializableDerived);
    }
}       

•    _valueobject является членом, на который всегда ссылается значение (например, встроенные типы, или структура или классы, для которых определены копирование и присвоение, и оператор >> и << определены как CArchive& oprator<<(CArchive& ar, const SValueClass& value) и CArchive& operator>>(CArchive& ar, SValueClass& value).

•    _pCObjectDerived является указателем на CObject или на производный класс CObject, для которого используются DECLARE_SERIAL и IMPLEMENT_SERIAL

•    _pESerializableDerived является указателем на класс, порождающийся виртуально от ESerializable из-за порождения от ETypeSerializabe<yourclass>, для которого также определяется глобальная переменная ETypeSerializable<yourclass>.


Реализация 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). Он сам сделает эти операции.


Пробное приложение

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

Приложение позволяет создать объект, определенный как классы множественного наследования, и выполнить сериализацию в и из файла. Приложение является приложением MFC, статически связанным с MFC, и с другой статической библиотекой по имени “Utility”, содержащей все классы, не связанные непосредственно с приложением, но относящиеся к общей функциональности. Эту библиотеку можно без ограничений использовать в любом вашем приложении.

Она управляет объектом, созданным на основе этой иерархии наследования:

Зеленые типы могут быть инстанцированы как объекты. Пунктирные линии указывают на виртуальное наследование.

CIntermediate1 может хранить (посредством интеллектуального указателя) производный объект CBase1, и CBase1 хранит обратный указатель на CBase3. То же самое верно для CBase2, CIntermediate2 и CBase3.

Существование интеллектуального указателя члена позволяет создать иерархию инстанцированных объектов. Приложение предоставляет команды, позволяющие создавать объекты разными способами, отображать объекты и сериализовывать их.

Файл demo.test, входящий в пример, содержит данные для создания следующей схемы объектов при загрузке.

и вывод отладки при загрузке файла выглядит так

. 00E18F40: class GE_::Data::CBase0: CBase0 ctor 
. 00E18D70: class GE_::Data::CBase1: CBase1 ctor
. 00E18DB0: class GE_::Data::CBase3: CBase3 ctor
. 00E18D70: class GE_::Data::CIntermediate1: CIntermediate1 ctor
. 00E18E28: class GE_::Data::CBase2: CBase2 ctor
. 00E18E68: class GE_::Data::CBase3: CBase3 ctor
. 00E18E28: class GE_::Data::CIntermediate2: CIntermediate2 ctor
. 00E18D70: class GE_::Data::CAssembled: CAssembled ctor
. 00E18D70: class GE_::Data::CAssembled: CAssembled::Serialize
.. 00E18D70: class GE_::Data::CAssembled: CIntermediate1::Serialize
... 00E18D70: class GE_::Data::CAssembled: CBase1::Serialize
.... 00E18F40: class GE_::Data::CAssembled: CBase0::Serialize
... 00E18DB0: class GE_::Data::CAssembled: CBase3::Serialize
... 00E19938: class GE_::Data::CBase0: CBase0 ctor
... 00E19768: class GE_::Data::CBase1: CBase1 ctor
... 00E197A8: class GE_::Data::CBase3: CBase3 ctor
... 00E19768: class GE_::Data::CIntermediate1: CIntermediate1 ctor
... 00E19820: class GE_::Data::CBase2: CBase2 ctor
... 00E19860: class GE_::Data::CBase3: CBase3 ctor
... 00E19820: class GE_::Data::CIntermediate2: CIntermediate2 ctor
... 00E19768: class GE_::Data::CAssembled: CAssembled ctor
... 00E19768: class GE_::Data::CAssembled: CAssembled::Serialize
.... 00E19768: class GE_::Data::CAssembled: CIntermediate1::Serialize
..... 00E19768: class GE_::Data::CAssembled: CBase1::Serialize
...... 00E19938: class GE_::Data::CAssembled: CBase0::Serialize
..... 00E197A8: class GE_::Data::CAssembled: CBase3::Serialize
..... 00E1A244: class GE_::Data::CBase0: CBase0 ctor
..... 00E1A160: class GE_::Data::CBase1: CBase1 ctor
..... 00E1A1A0: class GE_::Data::CBase3: CBase3 ctor
..... 00E1A160: class GE_::Data::CIntermediate1: CIntermediate1 ctor
..... 00E1A160: class GE_::Data::CIntermediate1: CIntermediate1::Serialize
...... 00E1A160: class GE_::Data::CIntermediate1: CBase1::Serialize
....... 00E1A244: class GE_::Data::CIntermediate1: CBase0::Serialize
...... 00E1A1A0: class GE_::Data::CIntermediate1: CBase3::Serialize
...... 00E1A974: class GE_::Data::CBase0: CBase0 ctor
...... 00E1A890: class GE_::Data::CBase1: CBase1 ctor
...... 00E1A8D0: class GE_::Data::CBase3: CBase3 ctor
...... 00E1A890: class GE_::Data::CIntermediate1: CIntermediate1 ctor
...... 00E1A890: class GE_::Data::CIntermediate1: CIntermediate1::Serialize
....... 00E1A890: class GE_::Data::CIntermediate1: CBase1::Serialize
........ 00E1A974: class GE_::Data::CIntermediate1: CBase0::Serialize
....... 00E1A8D0: class GE_::Data::CIntermediate1: CBase3::Serialize
.... 00E19820: class GE_::Data::CAssembled: CIntermediate2::Serialize
..... 00E19820: class GE_::Data::CAssembled: CBase2::Serialize
..... 00E19860: class GE_::Data::CAssembled: CBase3::Serialize
..... 00E1B340: class GE_::Data::CBase0: CBase0 ctor
..... 00E1B170: class GE_::Data::CBase1: CBase1 ctor
..... 00E1B1B0: class GE_::Data::CBase3: CBase3 ctor
..... 00E1B170: class GE_::Data::CIntermediate1: CIntermediate1 ctor
..... 00E1B228: class GE_::Data::CBase2: CBase2 ctor
..... 00E1B268: class GE_::Data::CBase3: CBase3 ctor
..... 00E1B228: class GE_::Data::CIntermediate2: CIntermediate2 ctor
..... 00E1B170: class GE_::Data::CAssembled: CAssembled ctor
..... 00E1B170: class GE_::Data::CAssembled: CAssembled::Serialize
...... 00E1B170: class GE_::Data::CAssembled: CIntermediate1::Serialize
....... 00E1B170: class GE_::Data::CAssembled: CBase1::Serialize
........ 00E1B340: class GE_::Data::CAssembled: CBase0::Serialize
....... 00E1B1B0: class GE_::Data::CAssembled: CBase3::Serialize
....... 00E1BD30: class GE_::Data::CBase0: CBase0 ctor
....... 00E1BB60: class GE_::Data::CBase1: CBase1 ctor
....... 00E1BBA0: class GE_::Data::CBase3: CBase3 ctor
....... 00E1BB60: class GE_::Data::CIntermediate1: CIntermediate1 ctor
....... 00E1BC18: class GE_::Data::CBase2: CBase2 ctor
....... 00E1BC58: class GE_::Data::CBase3: CBase3 ctor
....... 00E1BC18: class GE_::Data::CIntermediate2: CIntermediate2 ctor
....... 00E1BB60: class GE_::Data::CAssembled: CAssembled ctor
....... 00E1BB60: class GE_::Data::CAssembled: CAssembled::Serialize
........ 00E1BB60: class GE_::Data::CAssembled: CIntermediate1::Serialize
......... 00E1BB60: class GE_::Data::CAssembled: CBase1::Serialize
.......... 00E1BD30: class GE_::Data::CAssembled: CBase0::Serialize
......... 00E1BBA0: class GE_::Data::CAssembled: CBase3::Serialize
........ 00E1BC18: class GE_::Data::CAssembled: CIntermediate2::Serialize
......... 00E1BC18: class GE_::Data::CAssembled: CBase2::Serialize
......... 00E1BC58: class GE_::Data::CAssembled: CBase3::Serialize
...... 00E1B228: class GE_::Data::CAssembled: CIntermediate2::Serialize
....... 00E1B228: class GE_::Data::CAssembled: CBase2::Serialize
....... 00E1B268: class GE_::Data::CAssembled: CBase3::Serialize
.. 00E18E28: class GE_::Data::CAssembled: CIntermediate2::Serialize
... 00E18E28: class GE_::Data::CAssembled: CBase2::Serialize
... 00E18E68: class GE_::Data::CAssembled: CBase3::Serialize
... 00E1CC6C: class GE_::Data::CBase0: CBase0 ctor
... 00E1CB88: class GE_::Data::CBase2: CBase2 ctor
... 00E1CBC8: class GE_::Data::CBase3: CBase3 ctor
... 00E1CB88: class GE_::Data::CIntermediate2: CIntermediate2 ctor
... 00E1CB88: class GE_::Data::CIntermediate2: CIntermediate2::Serialize
.... 00E1CB88: class GE_::Data::CIntermediate2: CBase2::Serialize
..... 00E1CC6C: class GE_::Data::CIntermediate2: CBase0::Serialize
.... 00E1CBC8: class GE_::Data::CIntermediate2: CBase3::Serialize
.... 00E1D39C: class GE_::Data::CBase0: CBase0 ctor
.... 00E1D2B8: class GE_::Data::CBase2: CBase2 ctor
.... 00E1D2F8: class GE_::Data::CBase3: CBase3 ctor
.... 00E1D2B8: class GE_::Data::CIntermediate2: CIntermediate2 ctor
.... 00E1D2B8: class GE_::Data::CIntermediate2: CIntermediate2::Serialize
..... 00E1D2B8: class GE_::Data::CIntermediate2: CBase2::Serialize
...... 00E1D39C: class GE_::Data::CIntermediate2: CBase0::Serialize
..... 00E1D2F8: class GE_::Data::CIntermediate2: CBase3::Serialize
       

Обратите внимание, что:

•    CBase1, CIntermediate1 и CAssembled в одном и том же экземпляре имеют один и тот же адрес, но CBase0 (виртуальный) и CBase3 (и CBase2, и CIntermediate2 в CAssembled) имеют разные адреса (они не первые базовые классы: при создании полиморфного объекта MFC ожидает, что все базовые классы имеют один и тот же адрес, оттого нужны отдельные карты)
•    В CAssebled есть два CBase3, но они имеют разные адреса
•    Конструкторы выглядят линейными (без отступов): причина состоит в том, что конструкторы базовых классов вызываются автоматически перед входом в конструкторы производных классов. Вызовы сериализации имеют отступы, потому что они вызываются вложенно.

Как было сказано в начале, MFC не переделывалась. Возможно, тема непростая, но использование решения должно облегчить ее. Все-таки все сводится к написанию очень маленького кода в функциях сериализации (добавить две строки или вызвать один макрос – остается на ваше усмотрение).