100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C++

Serialization in MFC

How MFC's CObject::Serialize framework, CArchive, and the DECLARE_SERIAL/IMPLEMENT_SERIAL macros work together to persist objects to and from disk.

Collections and UtilitiesAdvanced11 min readJul 10, 2026
Analogies

The Serialization Framework

MFC's serialization framework centers on a virtual Serialize(CArchive& ar) method declared on CObject and overridden by any class that wants to persist itself. A CArchive object, constructed around a CFile in either store or load mode, drives the process: calling pDoc->Serialize(ar) on a document triggers a chain of Serialize() calls down through its contained objects. To participate fully, including support for dynamic creation when loading, a class must use the DECLARE_SERIAL(class) macro in its header and the matching IMPLEMENT_SERIAL(class, baseClass, schema) macro in its source file.

🏏

Cricket analogy: The Serialize() chain flowing from a document down through contained objects is like a scorecard summary that pulls detailed figures from each individual player's card, which in turn pulls from each ball bowled, cascading down through layers.

Implementing Serialize() in a Derived Class

A typical override calls the base class's Serialize() first, then checks ar.IsStoring() to decide whether to write or read its own members using operator<< and operator>>, which are overloaded for built-in types, CString, and other CObject-derived classes. Following this base-first pattern ensures that inherited state is always handled consistently regardless of how many levels deep the class hierarchy goes, and it mirrors the same discipline expected when writing constructors that initialize base classes before derived members.

🏏

Cricket analogy: Calling the base Serialize() first is like a team announcing its playing XI in the standard batting-order sequence before adding any match-specific notes, ensuring consistency no matter which team is reporting.

cpp
class CPlayerRecord : public CObject
{
    DECLARE_SERIAL(CPlayerRecord)
public:
    CString m_strName;
    int     m_nScore;

    virtual void Serialize(CArchive& ar) override
    {
        CObject::Serialize(ar); // base class first

        if (ar.IsStoring())
        {
            ar << m_strName << m_nScore;
        }
        else
        {
            ar >> m_strName >> m_nScore;
        }
    }
};

IMPLEMENT_SERIAL(CPlayerRecord, CObject, 1 /* schema */)

Serializing Collections and Pointers

CArchive has built-in support for serializing entire collections of CObject-derived pointers, such as CObArray or CTypedPtrArray, via a single ar << collection call, and it correctly tracks object identity: if two different pointers in memory reference the same underlying object, CArchive detects this using its internal MapObject bookkeeping and reconstructs the shared reference on load rather than duplicating the object into two separate copies. This makes CArchive suitable for object graphs with shared or cross-referenced nodes, not just simple trees.

🏏

Cricket analogy: CArchive tracking shared object identity is like a stats database recognizing that the same player object, Rohit Sharma, appears as both captain and opener in two different lineup slots, and storing him once rather than duplicating his record.

Versioning with Schema Numbers

The third argument to IMPLEMENT_SERIAL, the schema number, is written into the archive alongside the class's runtime type information, and GetObjectSchema() lets a Serialize() override read back which schema version produced the data currently being loaded. This allows a class to branch its loading logic, for example reading an extra field only if the schema number is 2 or higher, so that files saved by an older version of the application can still be loaded correctly by a newer version without corrupting or misinterpreting the data.

🏏

Cricket analogy: Branching on schema number is like a scoring app reading an old scoresheet format that predates DRS reviews and simply skipping the review-count field, while newer scoresheets include and parse that field.

CArchive is built on top of a CFile, buffering reads and writes for efficiency, so changes are not guaranteed to be fully on disk until the archive is closed, either explicitly via Close() or implicitly when its destructor runs. Always ensure the CArchive goes out of scope or is explicitly closed before assuming the underlying file is complete and safe to share or copy.

Forgetting the DECLARE_SERIAL(class) macro in the header, or the matching IMPLEMENT_SERIAL(class, baseClass, schema) macro in the source file, means the class lacks the runtime type information MFC's serialization framework needs to dynamically create instances while loading. This typically surfaces as an assertion failure or a corrupted read when the archive tries to reconstruct an object of that class from a CObArray or similar collection during load.

  • CObject::Serialize(CArchive&) is the virtual method every serializable class overrides.
  • DECLARE_SERIAL and IMPLEMENT_SERIAL macros enable runtime type info and dynamic creation during load.
  • Always call the base class's Serialize() before handling a derived class's own members.
  • IsStoring() and IsLoading() let one Serialize() method handle both save and load directions.
  • CArchive tracks shared object identity via internal MapObject bookkeeping, avoiding duplicate copies.
  • The schema number passed to IMPLEMENT_SERIAL enables backward-compatible version branching via GetObjectSchema().
  • CArchive buffers internally on top of CFile and must be closed to guarantee data is flushed.

Practice what you learned

Was this page helpful?

Topics covered

#MFCMicrosoftFoundationClassesStudyNotes#MicrosoftTechnologies#SerializationInMFC#Serialization#MFC#Framework#Implementing#StudyNotes#SkillVeris#ExamPrep