Search code examples
c++serializationboostc++17

Boost serialization of a std::vector of pointers. Error: "unregistered class - derived class not registered or exported"


I am using boost to (de)serialize some classes. All is well except for a class which has astd::vector<Artefact*> member (see below for definitions). I have problems with vectors of pointers.

I can (de)serialize single instances of class Artefact perfectly.

I cannot serialize a class which has a vector of pointers to class Artefact.

The boost error when I try to do so is:

"unregistered class - derived class not registered or exported"

I am getting some output. Archive file:

22 serialization::archive 19 1 0
0 1 0
1 1 0
2 0 0 66 0

Class Definitions

#include <boost/serialization/vector.hpp>
#include <boost/serialization/map.hpp>

...

class Tag {
public:
    bool tag = false;
};

class Artefact : public Tag {
public:
    std::vector<enjyn_Vertex> vertices;
    std::vector<unsigned int> indices;
    std::vector<enjyn_Texture*> textures;

    glm::vec3 position{ 0.0f };
    glm::vec3 scale{ 1.0f };
    glm::vec3 rotation{ 0.0f, 1.0f, 0.0f };
    float rotation_degrees{ 0.0f };
    glm::vec4 colour{ 1.0f, 1.0f, 1.0f, 1.0f };

    ...

    std::vector<Artefact> artefacts; // note, no problem serializing std::vector<Artefact> here

    ...

};


//Mainly static data and function members 
class State {
public:
...
    static std::map<std::string, bool> state;
...
}

class Event {
public:
...
    virtual void OnMouseFocus(); //Lots of virtual data members like this one here
..
}

//Large class with lots of data and function members.
class Enjyn : public Event, public State {
public:
    ...

    std::vector<Artefact*> artefacts;  // I believe this is the problem member 

    ...
}

Free Non-Intrusive (De)Serialization Functions

I have non-intrusive free serialize functions for all classes I want to archive including serialize functions for all base classes and non built in data members).

The serialize function below (de)serializes perfectly for single instances of Artefact:


#include <boost/serialization/export.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
...
void serialize(Archive& ar, Artefact& a, const unsigned int version) {
    //ar.template register_type<Artefact>(); ; commented as classes exported see below
    ar& boost::serialization::base_object<enjyn_Tag>(a);
    ar& a.vertices;
    ar& a.indices;
    ar& a.textures;

    ar& a.position;
    ar& a.scale;
    ar& a.rotation;
    ar& a.rotation_degrees;
    ar& a.colour;
    ar& a.aabb;
    ar& a.bsphere;
    ar& a.tex_offset_x;
    ar& a.tex_offset_y;
    ar& a.tex_scale_x;
    ar& a.tex_scale_y;
    ar& a.name;
    ar& a.artefacts; //works fine for vector of non-pointer vector of Artefact.
}

However when I try to archive an object with a std::vector<Artefact*>, I get: "unregistered class - derived class not registered or exported"

void serialize(Archive& ar, Enjyn& e, const unsigned int version)
{
    //ar.template register_type<enjyn>(); commented as classes exported see below:
    ar& boost::serialization::base_object<Event>(e); // not sure whether this is necessary, no tracking requried of base classes.
    ar& boost::serialization::base_object<State>(e);
    ar.template register_type<Artefact>();

    ...

    ar& e.artefacts; //This is a std::vector<Artefact*> which I cant archive
                        //if i remove this and add include any 'normal' non-pointer
                        //built in types the serialization appears to work perfectly.
    ...
}

Serializing Program Functions

I am serializing single instances of Artefact objects perfectly like this:


if (e.primary_artefact) { //primary_artefact is a pointer to an instance of Artefact
    try {

        std::string filepath = "test_filename";
        std::ofstream o(filepath);
        {
            boost::archive::text_oarchive oa(o);
            oa << *e.primary_artefact;
        }
    }
    catch (std::ifstream::failure e) {
        std::cerr << "ERROR:IFSTREAM: " << e.what() << std::endl;
    }
    catch (boost::archive::archive_exception e) {
        std::cerr << "ERROR:BOOST SERIALISATION: " << e.what() << std::endl;
    }
}

Attempting to serialize class Enjyn using this function creates error if I include Enjyn.artefacts (std::vector<Artefact*>) in the serialize function:


void serialize_enjyn(enjyn& e, std::string& name) 
    try {

        std::string filepath = name;
        std::ofstream o(filepath);
        {
            boost::archive::text_oarchive oa(o);
            oa << e;
        }
    }
    catch (std::ifstream::failure e) {
        std::cerr << "ERROR:IFSTREAM: " << e.what() << std::endl;
    }
    catch (boost::archive::archive_exception e) {
        std::cerr << "ERROR:BOOST SERIALISATION: " << e.what() << std::endl;
    }
}


I went mad and exported GUIDS for all types in my main program file so all relevant types could be tracked within the archive. Not that this is particularly necessary. Only tracking of '''class Artefact''' is required.


#include <boost/serialization/export.hpp>
BOOST_CLASS_EXPORT_GUID(glm::vec2, "vec2")
BOOST_CLASS_EXPORT_GUID(glm::vec3, "vec3")
BOOST_CLASS_EXPORT_GUID(glm::vec4, "vec4")
BOOST_CLASS_EXPORT_GUID(Artefact, "enjyn_Artefact")
BOOST_CLASS_EXPORT_GUID(Vertex, "enjyn_Vertex")
BOOST_CLASS_EXPORT_GUID(Texture, "enjyn_Texture")
BOOST_CLASS_EXPORT_GUID(Tag, "enjyn_Tag")
BOOST_CLASS_EXPORT_GUID(State, "enjyn_State")
BOOST_CLASS_EXPORT_GUID(Event, "enjyn_event")
BOOST_CLASS_EXPORT_GUID(Enjyn, "enjyn")

int main(int argc, char* argv[]){

Discussion

Note, the Artefact object is used as a base for other classes. I have not registered those classes or written serialization functions for them as Artefact has no virtual functions, I am trying to serialize Artefact directly (though indirectly also via pointers), and the documentation for registration states:

It turns out that the kind of object serialized depends upon whether the base class (base in this case) is polymophic or not. If base is not polymorphic, that is if it has no virtual functions, then an object of the type base will be serialized. Information in any derived classes will be lost. If this is what is desired (it usually isn't) then no other effort is required.

The problem appears to be with the member std::vector<Artefact*> artefact in Enjyn. When I remove this from the serialize function for Enjyn, Enjyn serializes perfectly. I am obviously missing something fundemental to do with:

pointers, std::vector, and derived types and base classes from the boost documentation.

Please help!


Solution

  • If you actually took the time to make that a correct, self-contained example, you may find that there is no problem:

    Live On Coliru

    #include <boost/serialization/map.hpp>
    #include <boost/serialization/vector.hpp>
    #include <boost/serialization/array.hpp>
    #include <iostream>
    
    // mocking stuff
    namespace glm {
        struct vec3 {
            std::array<float, 3> a = {0,0,0};
            bool operator==(vec3 const& rhs) const = default;
        };
        struct vec4 {
            std::array<float, 4> a = {0,0,0,0};
            bool operator==(vec4 const& rhs) const =default;
        };
    } // namespace glm
    
    struct enjyn_Vertex {
        bool operator==(enjyn_Vertex const&) const = default;
    };
    struct enjyn_Texture {
        bool operator==(enjyn_Texture const&) const = default;
    };
    
    struct enjyn_Tag {
        bool tag                                = false;
        bool operator==(enjyn_Tag const&) const = default;
    };
    
    class Artefact : public enjyn_Tag {
      public:
        std::vector<enjyn_Vertex>   vertices;
        std::vector<unsigned int>   indices;
        std::vector<enjyn_Texture*> textures; // not owned, so leaked?
    
        glm::vec3 position{0.0f};
        glm::vec3 scale{1.0f};
        glm::vec3 rotation{0.0f, 1.0f, 0.0f};
        float     rotation_degrees{0.0f};
        glm::vec4 colour{1.0f, 1.0f, 1.0f, 1.0f};
    
        std::vector<Artefact> artefacts;
    
        bool operator==(Artefact const& rhs) const {
            if (!enjyn_Tag::operator==(rhs))
                return false;
    
            auto tie = [](Artefact const& a) {
                return std::tie(a.vertices, a.indices, /*a.textures,*/ a.position,
                                a.scale, a.rotation, a.rotation_degrees, a.colour,
                                a.artefacts);
            };
            if (tie(*this) != tie(rhs))
                return false;
            // compare textures indirectly
            if (textures.size() != rhs.textures.size())
                return false;
            for (size_t i = 0; i<textures.size(); ++i)
                if (*textures[i] != *rhs.textures[i])
                    return false;
    
            return true;
        }
    
      private:
    };
    
    //Mainly static data and function members 
    class State {
      public:
        static std::map<std::string, bool> state;
    
        bool operator==(State const&) const = default;
    };
    /*static*/ std::map<std::string, bool> State::state;
    
    class Event {
      public:
        virtual void OnMouseFocus() {
        } // Lots of virtual data members like this one here
        bool operator==(Event const&) const = default;
    };
    
    //Large class with lots of data and function members.
    class Enjyn
        : public Event
        , public State {
      public:
        // Rule Of Three...
        ~Enjyn() { for (auto a : artefacts) delete a; }
        Enjyn()             = default;
        Enjyn(Enjyn const&) = delete;
        Enjyn(Enjyn&&)      = default;
        Enjyn& operator=(Enjyn const&) = delete;
    
        std::vector<Artefact*> artefacts;
    
        bool operator==(Enjyn const& rhs) const {
            if (! Event::operator==(rhs)) return false;
            if (! State::operator==(rhs)) return false;
    
            // compare artefacts indirectly
            if (artefacts.size() != rhs.artefacts.size())
                return false;
            for (size_t i = 0; i<artefacts.size(); ++i)
                if (*artefacts[i] != *rhs.artefacts[i])
                    return false;
    
            return true;
        }
    };
    
    #include <boost/serialization/export.hpp>
    #include <boost/archive/text_oarchive.hpp>
    #include <boost/archive/text_iarchive.hpp>
    namespace boost::serialization {
        template <typename Archive> void serialize(Archive& ar, enjyn_Vertex&, unsigned) { }
        template <typename Archive> void serialize(Archive& ar, enjyn_Texture&, unsigned) { }
        template <typename Archive> void serialize(Archive& ar, enjyn_Tag& t, unsigned) { ar& t.tag; }
        template <typename Archive> void serialize(Archive& ar, glm::vec3&v, unsigned) { ar& v.a; }
        template <typename Archive> void serialize(Archive& ar, glm::vec4&v, unsigned) { ar& v.a; }
        template <typename Archive> void serialize(Archive& ar, Artefact& a, unsigned)
        {
            //ar.template register_type<Artefact>(); ; commented as classes exported see below
            ar& boost::serialization::base_object<enjyn_Tag>(a);
            ar& a.vertices;
            ar& a.indices;
            ar& a.textures;
    
            ar& a.position;
            ar& a.scale;
            ar& a.rotation;
            ar& a.rotation_degrees;
            ar& a.colour;
            //ar& a.aabb;
            //ar& a.bsphere;
            //ar& a.tex_offset_x;
            //ar& a.tex_offset_y;
            //ar& a.tex_scale_x;
            //ar& a.tex_scale_y;
            //ar& a.name;
            ar& a.artefacts; // works fine for vector of non-pointer vector of Artefact.
        }
        template <typename Archive> void serialize(Archive& ar, Event& e, unsigned)
        {
        }
        template <typename Archive> void serialize(Archive& ar, Enjyn& e, unsigned)
        {
            ar& e.state; // warning: serializing statics may not do what you want
            ar& e.artefacts;
        }
    } // namespace boost
    
    Enjyn bake_me_one_with_everything();
    
    int main() {
        Enjyn const original = bake_me_one_with_everything();
    
        std::stringstream ss;
        {
            boost::archive::text_oarchive oa(ss);
    
            oa << original;
        }
    
        std::cout << ss.str() << "\n";
    
        {
            boost::archive::text_iarchive ia(ss);
    
            Enjyn e;
            ia >> e;
            std::cout << "Roundtrip equal: " << std::boolalpha << (e == original)
                      << std::endl;
        }
    }
    
    #include <random>
    Enjyn bake_me_one_with_everything()
    {
        Enjyn e;
    
        std::mt19937 urbg{std::random_device{}()};
        auto         frand =
            std::bind(std::uniform_real_distribution<float>{}, std::ref(urbg));
        auto r10 =
            std::bind(std::uniform_int_distribution<>{1, 10}, std::ref(urbg));
    
        for (int i = 0; i < r10(); ++i) { // very sloppy loop
            auto& a = *e.artefacts.emplace_back(new Artefact());
            a.artefacts.resize(r10());
    
            a.colour           = {frand(), frand(), frand()};
            a.position         = {{frand(), frand(), frand()}};
            a.rotation         = {{frand(), frand(), frand()}};
            a.rotation_degrees = frand() * 360;
            a.scale            = {{frand(), frand(), frand()}};
            a.tag              = r10() % 2;
    
            std::generate_n(back_inserter(a.indices), r10(), r10);
            a.vertices.assign(r10(), enjyn_Vertex{});
            a.textures.assign(r10(), new enjyn_Texture{});
        }
        return e;
    }
    

    Prints things like

    enter image description here

    Your princess is in another castle.

    Imaging The Flaws

    I notice a lot of "gratuitous" use of inheritance (e.g. to inherit static data members), but specifically you inherit Enjyn from Event, which is a polymorphic type.

    So my best guess is that this contains your actual issue.

    Otherwise, you should be able to go from this live sample.

    Caveats

    Your code presentation shows some ordering that indicates you may be separating code across source files. In that case, don't fall into the trap of registering your types in a file that doesn't include all the relevant archive types BEFORE the export defitions: docs

    BOOST_CLASS_EXPORT in the same source module that includes any of the archive class headers will instantiate code required to serialize polymorphic pointers of the indicated type to the all those archive classes. If no archive class headers are included, then no code will be instantiated.