Introduction
As a C programmer on Windows since W3.0, I found MFC to be a great wrapper for W32 API in a C++ environment. However, I also found MFC to be almost �poor� in term of C++ implementation because of certain limitations (probably inherited because compatibility with the past).
- The first is that MFC is completely unaware of namespaces.
- The second is that it comes with its own collections, and don�t consider STL as a convenient library for this purpose
- The third is that MFC in not designed for multiple inheritance (certain programmers consider that insane, but they also use it when inheriting form �interface�, that are nothing more than abstract �class� or �struct� �)
- The fourth id that it makes an exaggerate use of macros, making the use of types with composite names difficult (try to
DECLARE_SERIAL
a template or a class into a namespace ...)
Going through this article I propose some alternatives to MFC model for type declarations in namespaces, use of templates for serialization, safe serialization with multiple inheritance classes etc.
Background
It is important for the reader an experience in programming with MFC, and also a knowledge on MFC source code. I'm not going to teach MFC or rewrite it. I simply want to go ahead, leaving MFC to do what it is capable to do, and do something else where something more is needed.
In my samples, I�ll make use of namespaces, STL and some �helper classes�. For some of them I also wrote some independent articles. It is not required a knowledge on those articles, apart if you are looking for details. (I�ll remand you to the links where needed).
Limitations of MFC
Mfc and RTTI
In the early days of MS C++, "strange things" like templates and runtime type information where not supported by the language. So MFC, because of the need of a runtime type information mechanism, starts implementing its own.
Today, even with RTTI supported by the language this mechanism still exist, for two reasons. The first is �compatibility with the past�, and the second is �dynamic creation�: in fact what MFC does is not only RTTI, but also a way to dynamically create polymorphic objects.
Although there is a certain area of overlap, both the mechanism defect in something:
- MFC
DECLARE_SERIAL
is designed explicitly for single inheritance objects, it is based on macros that makes preprocessing with type names (token pasting is done to transform a class name into the name of a static variable and into a string: it doesn�t work, for example, with templates or with composite names, like calss into namespaces)
- RTTI is deigned to provide runtime type identification and conversion (
dynamic_cast
) but cannot � alone � do dynamic creation.
The good news, however, is that the two mechanism don�t conflict: enabling RTTI allow to use dynamic_cast
for both single and multiple inheritance object (MFC mechanism never knows about inheritance other than the first) and CObject
derivation with DECLARE/IMPLEMENT_SERIAL
allows dynamic creation. But it�s up to you to manage the serialization of the bases.
This last fact creates some problem when a base is inherited more than once or is virtually inherited by more than one class.
Hence, we need to complement RTTI and MFC to support dynamic creation and serialization of multiple inheritance objects. Possibly avoiding macros.
MFC and Namespaces
Mfc classes are all defined in the global namespace.
And it�s not easy to place them into a namespace other than global: MFC code is full of function calls like �::function(...)
�: move that function in a namespace and you�re stuck. However, even if thinking to let MFC where it is, and define only our classes into namespaces we still have some difficulties: MFC macros.DECLARE_SERIAL
, DECLARE_MESSAGEMAP
, etc. take a class name as a parameter. IMPLEMENT_SERIAL
, BEGIN_MESSAGE_MAP
etc. take class name and base class name as parameters. To make all this working, we have to arrange namespaces so that class names in macros will always be �simple names�.
One possible solution is here:
#pragma once
namespace GE_{namespace App{
class
CAppFrameWnd : public CFrameWnd
{
public:
CAppFrameWnd(void);
~CAppFrameWnd(void);
DECLARE_MESSAGE_MAP()
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
};
}}
#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;
}
}}
This will compile correctly. But beware: all wizards tends to put all the generated code at global level, specifying the full name.
When I created the message map (by adding a WM_CREATE
handler trough the properties window) the code appeared this way:
#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;
}
and compiles giving error C2327 on GE_::App::CAppFrameWnd::OnCreate
This is because BEGIN_MESSAGE_MAP
doesn�t recognize the "::
" as part of the name as a first parameters, and brakes all the compiler preprocessing. To get out from this situation, I removed the name qualifiers and moved the code inside the proper namespace. It should be not necessary for member functions, but I preferred a uniform coding.
Mfc and serialization
There is another problem: serialization. Imagine two classes with the same name in different namespaces, both with DECLARE/IMPLEMENT_SERIAL
.
Both the classes will supply the same simple name to the macros, but classes and static members goes in different namespaces. The compiler works correctly and so the execution. But When you serialize a pointer to one of this class here comes the problem: when serializing a pointer to an object, if the object is not yet been serialized, MFC saves on file the type name of the object, in order to know, at load time, which CRuntimeClass
to use to call CreateObject
(the type name is used as a key).
But the �name� is ... what you write in the macros parameters! There are, in the system, two CRuntimeClass
with a same m_lpszClassName
. Hence, on loading, MFC can create objects confusing their types.
The proposed solution
Is in �Factory.h� and �Factory.cpp�. A set of RTTI based template classes to manage serialization, rather than macros.
What's SFactory ?!
It�s the �alter ego� of CRuntimeClass
. It is a struct, containing a const type_info*
(it comes from <typeinfo.h>, RTTI must be enabled) that is used to provide a type name in string form.
SFactory
is chained into an internal std::list
on construction and destruction, and can be found by the type-name string using the FindFactory
static function. It also defines the abstract member function NewObject
, but doesn�t implement it.
The full functionality are implemented in a template class, derived from SFactory
, STypeFactory<E>
. It�s constructor initialize the type_info
of the base to point to the typeid(E)
, and NewObject
is overridden to return new E
.
Normally, you will never manipulate directly those classes, instead, you will interact with them through other classes: ESerializable
and ETypeSerializable<E>
.
ESerializable
is a class designed to be virtually inherited form every class you may want to be serializable (virtual because your class may have various bases each of which may be itself serializable: but we need only one ESerializable
instance per object).
It defines an abstract functions: virtual void Serialize(CArchive& ar)
(Do have I already seen it somewhere ?) and a virtual SFactory* GetFactory()
(here it comes again!), that by default returns the value of a member variable.
You will not normally take care of this class. Instead, you will take care of its derived template: ETypeSerializable<E>
.
It virtually inherit ESerializable
, and associate a STypeFactory<E>
to the E type, setting the member variable of his base. GetFactory
, so, returns the STypeFactory<E>
.
Now:
- To make a class serializable: you must derive that class from
ETypeSerilizable<yourclass>
as the last base class: this is necessary to make GetFactory()
return the correct value (if it is an MFC class,let the first base to be an MFC base) This is the equivalent for DECLARE_SERIAL
- To make it loadable: you must declare at global level (inside an unnamed namespace it's very good) a variable of type
ETypeSerilizable<yourclass>
. This is because to make the load mechanism working, at least one factory for your type must exist. Even if you've not yet loaded or created any object of yourclass
. This is the equivalent for IMPLEMENT_SERIAL
Here's a sample:
class YourClass: public YourBase, public ETypeSerializable<YourClass>
BE THE
LAST BASE
{
};
namespace {
ETypeSerializable<YourClass> g_yourclassfactory;
}
When you have to serialize a pointer, just call SavePtr
(on storing) and LoadPtr
(on loading) passing a pointer variable.
NOTE: I didn't define operators like << and >> because they conflict with the pointer save and load operator of MFC.
Because I used templates, not macros, no problem will arise with composite names like names into namespaces or template classes.
However, if you want to make template classes serializable, remember that you need a ETypeSerilizable<yourtemplateclass<yourparameters> >
for each template instantiation you have to use in your program.
For example, if this is your header
template<class T>
class YourTemplate:
public ETypeSerializable<YourTemplate<T> >
{
};
and you are going to use YourTemplate
for UINT
and CString
, you should, in a cpp file, at global level, create the following
namespace {
ETypeSerializable<YourTemplate<UINT> > g_yourtemplateForUint;
ETypeSerializable<YourTemplate<CString> > g_yourtemplateForCString;
}
Serialization maps
When saving pointers, to avoid to serialize a same object more times and keep track of circularity, I had to associate to the serialization archive some maps associating to an object a tag (and vice versa).
This is (more or less) what MFC does in it�s CArchive
implementation, but those maps are designed for CObject
derived, not for ... ESerializable
virtually derived.
SArchiveMaps
implements what is required. It must be instantiated associated to a CArchive
before starting to serialize/deserialize ESerializable
elements and destroyed after finished.
The simpler way to do this, in MFC application, is inside your derived CDocument::Serialize
implementation: Documents, Frames and Views will always be created by MFC via CDocTemplate
and CRuntimeClass
. When saving and loading CDocument::Serialize
is always called. So ...
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
is meant to be a member that is always referred by value (like built-in types, or struct or classes for which copy and assignment are defined, and operator >> and << are defined as CArchive& oprator<<(CArchive& ar, const SValueClass& value)
and CArchive& operator>>(CArchive& ar, SValueClass& value)
.
_pCObjectDerived
is meant to be a pointer to CObject
or to a CObject
derived class, for which DECLARE_SERIAL
and IMPLEMENT_SERIAL
are used
_pESerializableDerived
is meant to be a pointer to a class that derives virtually from ESerializable
, because of a derivation from ETypeSerializabe<yourclass>
, for which a ETypeSerializable<yourclass>
global variable is also defined.
Serialize
implementation
CObject
and ESerializable
serialization don�t conflict: you can have a class that derive from both CObject
and ETypeSerilizable
(a number of times), however, avoid to implement both DECLARE/IMPLEMENT_SERIAL
and also ETypeSerializable
instance, because you cannot � for a same object � save it sometimes in a way and sometimes in anoter: the maps that take care of objects identity are different (are embedded in CArchive
for CObject*
and implemented by SArchiveMaps
for ESerializable*
) so you risk to save your object twice and � on load � to load two different identical copies.
Apart this warning, however, there is another more serious problem: multiple bases.
Suppose class C
deriving from A
and B
, both deriving from D
and virtually from E
. Suppose all these classes can exist also as stand alone classes and are all serializable (they all derive from their respective ETypeSerializable<>
class).
In the following picture you can see the hierarchy diagram.
There are two instances of D
and only one instance of E
(but it is referred twice). This leads to two problems.
First: avoid to serilize the already serialized components
Suppose to implement C::Serialize
by calling A::Serialize
and B::Serialize
.
Suppose A::Serialize
calls D::Serialize
and E::Serialize
. And B::Serialize
to call D::Serilize
and E::Serilize
.
Boom! Serialize C
and you wiil serialize E
twice.
In other words, we may have the need to control, before serializing something, if that �something� has already been serialized. But this is not the problem of circular pointers: E
is not referred by A
and B
with a pointer.
Of course, you can think to don�t call E::Serialize
from B
, but what happens when B
exist by itself (not as a base for C
) ? You will miss it�s E
component.
To solve this problem I�d also implemented in SArchiveMaps
, a map whose key is formed by a pair of pointers: one to SSerializWatchDoc
and the other to ESerializable
. The value is an UINT
and it�s just a counter.
SSerilizeMaps
, that already provides a MapObject
function (it is called by �Load
� and �Save
�, but you can call it as well in the cases that similarly requires to call CArhive::MapObject
for CObject derived), also provide a MapWatchDog
, taking as arguments a SSerializeWatchDog and
an ESerializable
.
SSerializeWatchDog
, in turn, provides only one function that is
bool Locked(CArchive& ar, LPVOID pInstance)
.
This function retrieve the maps associated with the archive, and call MapWatchDog
passing itself an the supplied LPVOID
. MapWatchDog
, searches (and allocate) the entry and increments the counter. Locked
return true
if the counter is returned as greater than 1.
Note that, being the derivations from E
virtual, whatever subcomponent of C
you cast to E*
, you will obtain always the same pointer value (the "this" pointer for E
is the same, but there are two different "this" pointers for D
)
Multiple serialization of bases can be so avoided by creating a static SSerializeWatchDog
variable at the beginning of a Serilize
function, and returning immediately if Locked(ar, this)
returns true
: that means that you already executed that body (represented by the watchdog variable) over that instance (represented by the this
, casted to LPVOID
)
All the data structure are deleted when SArchiveMaps
is destroyed. Here are samples of C
B
and E
serializations
void C::Serialize(CArchive& ar)
{
static SSerializeWatchDog wd;
if(wd.Locked(ar, this)) return;
A::Serialize(ar);
B::Serialize(ar);
if(ar.IsStoring())
else
}
void B::Serialize(BArchive& ar)
{
static SSerializeWatchDog wd;
if(wd.Locked(ar, this)) return;
E::Serialize(ar);
D::Serialize(ar);
if(ar.IsStoring())
else
}
void E::Serialize(BArchive& ar)
{
static SSerializeWatchDog wd;
if(wd.Locked(ar, this)) return;
if(ar.IsStoring())
else
}
In a single shot, the macro GE_SERIALIZE_CHECK_MULTIPLE()
just implements the first two rows of code of the serialize functions.
Second: distinguish between component of the same type
Suppose you have two D*
, pointing respectively to the two different D
component of a same C
object (the one in the previous picture). When saving the first pointer, the entire C
is serialized, while when saving the second only a map tag should be saved.
But when loading back, we cannot load C
and then convert to a D*
, because this conversion will be ambiguous. For this reasons, map tag cannot be generated only for entire objects (like MFC does) but requires to be generated for every component.
The solution of the problem is to map generic LPVOID
s (the two D*
are different) and create a tag for each during C
serialization. This is easily done, again, by SArchiveMaps::MapWatchDog
: this function is called in a Serialize
body passing the �this� pointer: to solve the problem we simply call MapObject(pInstance)
.
Thus, to solve the multiple base problems, always in every Serialize
body, define a static SSrializeWatchDog
and call Locked
. This will map the object and cheek if the body has been already executed for that instance. If it returns true, return immediately from that body.
If you like, use GE_SERIALIZE_CHECK_MULTIPLE(CArchive)
macro. It will do itself those operations.
Sample application
As a sample for all this theory, I designed a very simple application showing the concept here treated.
Of course the application is not designed to be �beautiful�: it simply allows to create object defined as multiple inherited classes and perform serialization to and from file. The application is an MFC application linked statically to MFC, and to another static library I called �Utility�, that contains all the classes not directly pertinent to the application itself, but related to general functionality. This library can be used freely in every application you would like.
It manages object created from this inheritance hierarchy:
Green types are the ones that can be instantiated as objects. Dashed lines indicates virtual inheritance.
CIntermediate1
can hold (through a smart pointer) a CBase1
derived object, and CBase1
holds a back-pointer to a CBase3
. Same thing for CBase2
, CIntermediate2
and CBase3
.
The existence of this member smart pointer allow to create a hierarchy of instantiated objects. The application provide commands to allow you to create them in a variety of ways, to display them and to serialize them.
The file demo.test I included in the sample contains data to produce the following object scheme when loaded.
and the debug output, while loading the file, looks like this
. 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
Note that:
CBase1
, CIntermediate1
and CAssembled
, in a same instance have the same address, but CBase0
(that's virtual) and CBase3
(and also CBase2
and CIntermediate2
in a CAssembled
) have not (they're not the first bases: having all the bases a same address is what MFC expect when creating an polimorphic object: that's why we needed separate maps)
- In a
CAssebled
there are two CBase3
, but they've different addresses
- Constructors appear to be "flat" (not indented): this is because base constructor are called automatically before entering derived class constructors. Serialize calls appear to be indented because they are called in a nested way.
As I told in the beginning, I didn�t rewrite MFC. I went ahead with MFC to do something more of what MFC does, still relying on MFC for window classes, Application framework and Doc / View architecture.
May be the subject it�s not easy, but the usage of the solution, in fact should make it easy. After all, everything reduces to very few coding in your serialize functions (just add two rows or call one macro: whatever you prefer)