c++serializationvisual-c++mfccereal

serialize with Cereal in MFC


I'm trying to use cereal (C++ serialization library) in the MFC project.

It works fine if I add an "internal serialize function" inside of the custom struct that I want to serialize. But, if I separately define "external serialize function" outside of the struct, then it gives a compilation error: "cereal could not find any output serialization functions for the provided type and archive combination."

So, it seems the external serialize functions that I defined are not found correctly, and the documentation says:

External serialization functions should be placed either in the same namespace as the types they serialize or in the cereal namespace so that the compiler can find them properly.

But, my custom struct and the external serialize functions are commonly defined in the project's View class header file:

class CmyprojectView : public CView
{
    // other declarations...

    // Custom struct that I want to serialize.
    struct MyStruct { ... }

    // External serialize function
    template <class Archive>
    void serialize(Archive & ar, CmyprojectView::MyStruct & s){
        ar(
            CEREAL_NVP(s.x),
            CEREAL_NVP(s.y),
            CEREAL_NVP(s.z)
        );
    }
}

Any advice on why my serialize functions are not found?


Thanks to @IInspectable's comments, I decided to let the custom struct (to be serialized) stay in the View class header file (CmyprojectView.h), and move the "external serialize function"(a term coined by cereal library) to another header file, wrapped in cereal namespace (it also works without the namespace, though).

One remaining question is, even if the struct to be serialized is declared as private, the external serialize function can access it - how is this possible?

Any additional advice would be appreciated!

P.S. When using cereal, don't forget to use the RAII concept, i.e. wrap it with { } such as {cereal::JSONOutputArchive oarchive(ss); oarchive(s);}, although it's not shown in IInspectable's sample code since the main function's scope is enough and also for brevity I guess.


Solution

  • Intro

    The cereal library is a flexible serialization framework. One of its features is extensibility, making its algorithms readily available to user-defined types. Extensibility support is generously provided by repurposing C++'s overload resolution rules.

    Problem Statement

    The code in question fails to play by those rules.

    Before diving in too deep, here's a minimal version of the code that reproduces the issue1:

    #include <cereal/archives/json.hpp>
    
    #include <sstream>
    
    struct CmyprojectView {
        struct MyStruct {
            float x;
        };
    
        template <class Archive>
        void serialize(Archive& ar, CmyprojectView::MyStruct& s) {
            ar(CEREAL_NVP(s.x));
        }
    };
    
    int main() {
        auto s { CmyprojectView::MyStruct() };
    
        std::stringstream ss;
        cereal::JSONOutputArchive oarchive(ss);
        oarchive(s);
    }
    

    Dumping the above into a newly created console application project (and setting up the cereal library) produces the following compiler diagnostic:

    error C2338: static_assert failed: 'cereal could not find any output serialization functions for the provided type and archive combination. [...]

    The diagnostic is spot-on but doesn't otherwise elucidate where the compiler went looking for a serialization function. Thankfully, the documentation covers the rules for "external serialization":

    External serialization functions should be placed either in the same namespace as the types they serialize or in the cereal namespace so that the compiler can find them correctly.

    This is a bit hand-wavy2 but gets the main point across: If the serialize() function template is not a class member of the class being serialized it needs to be a free function (template) that lives in one of two namespaces. In the sample above, however, the serialize() function template is a class member of an unrelated class.

    Speculation

    I suppose part of the confusion here is that C++ uses the same token (::) to delineate class and namespace hierarchy boundaries. CmyprojectView::serialize could refer to either a free function (template) in the CmyprojectView namespace, or a class member of the CmyprojectView class. In the sample above it is the latter, and cereal won't consider it as an external serialization function for the MyStruct class (whose fully qualified name is ::CmyprojectView::MyStruct).

    Solution

    To resolve the issue, the serialize() function template needs to be either a free function template in the same namespace that MyStruct lives in, or a class member of the MyStruct class.

    Establishing the first option is as simple as slapping the friend keyword onto the function template definition:

    template <class Archive>
    friend void serialize(Archive& ar, CmyprojectView::MyStruct& s) {
        ar(CEREAL_NVP(s.x));
    }
    

    This accomplishes two things:

    1. It grants serialize() access to the private parts of the enclosing class (CmyprojectView).
    2. Crucially, it also turns what used to be a class member into a free function (template)3.

    With just this change, things will (magically4) start to compile. While there is nothing technically wrong with this code I will say that it is difficult to read than it needs to be. A solution that's easier to comprehend is to make serialize() a class member of MyStuct. This is what cereal calls "internal serialization":

    struct MyStruct {
        float x;
    
        template <class Archive>
        void serialize(Archive& ar) {
            ar(CEREAL_NVP(x));
        }
    };
    

    This moves the serialization code into the class it is operating on, making it easy to discover. The code will also continue to work regardless of whether MyStruct is a nested class, or lives in an arbitrary namespace.

    The only wrinkle with this design is that the serialize() function template needs to be publicly visible, making it look like it was part of the class' regular interface as opposed to an artifact of the serialization framework.

    This can be addressed by making cereal::access a friend:

    struct MyStruct {
        float x;
    
    private:
        friend cereal::access;
    
        template <class Archive>
        void serialize(Archive& ar) {
            ar(CEREAL_NVP(x));
        }
    };
    

    That's better but feels a bit clumsy still. serialize() is a class member of MyStruct, and shows up in documentation or code completion hints for this class. We can fix this by going full circle to the first solution above, and re-apply the hidden friend idiom. Except, this time around the implementation goes into the MyStruct class:

    struct MyStruct {
    private:
        float x;
    
        template <class Archive>
        friend void serialize(Archive& ar, MyStruct& s) {
            ar(CEREAL_NVP(s.x));
        }
    };
    

    Nothing changed concerning readability: This is still a challenge to comprehend. serialize() looks a lot like a class member, but isn't.

    There are, however, several benefits to this:

    Conclusion

    All proposed solutions solve the issue at hand. There are trade-offs to be made, and the choice of design is primarily down to personal preference.


    1 MFC types have been stripped. The issue is unrelated to the MFC, and the solution(s) work with or without the MFC getting involved.

    2 I don't know the exact semantics of "should", nor why the global namespace is (allegedly) not considered.

    3 Into some namespace. I'm confident that the rules that govern this are well-defined and were implemented in good faith. Honestly, though, life is too short to follow down that rabbit hole.

    4 See The Power of Hidden Friends in C++.