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 может создать объекты, перепутав их типы.