Search code examples
c++iostream

Reading subclass instance of superclass from iostream. How does >> operator know which subclass?


I have a superclass Element with multiple subclasses, let's call them A and B. I want to overload << and >> so I can save and load my objects.

class Element
{
public:
   Element();
   int superProperty;
   virtual void write(iostream &out)
   {
      out << superProperty;
   }
   virtual void read(iostream &in)
   {
      in >> superProperty;
   }
};

iostream operator<<(iostream &out, const Element &elt)
{
  elt.write(out);
  return(out);
}

iostream operator>>(iostream &in Element &elt)
{
  elt.read(in);
  return(in);
}

class A : public Element
{
public:
  A();
  int subProperty;
   void write(iostream &out)
   {
      Element::write(out);
      out << subProperty;
   }
   void read(iostream &in)
   {
      Element::read(in);
      in >> subProperty;
   }
};

class B : public Element
{
public:
  B();
  double subProperty;
   void write(iostream &out)
   {
      Element::write(out);
      out << subProperty;
   }
   void read(iostream &in)
   {
      Element::read(in);
      in >> subProperty;
   }
};

With these definitions, I can easily write out a file of my Elements, writing each one as

iostream mystream;
Element e;
...
mystream << e;

Where I'm stuck is reading them back in. I want it to look like this:

iostream mystream;
Element *pe;
...
pe = new Element();  // the problem is right here
mystream >> *pe;

But that won't work because I don't know if the element I'm about to read is an Element or an A or a B. (In my application, I never actually instantiate an Element. All objects are one of the subclasses.) I resorted to writing out a char to indicate the class...

if (dynamic_cast<A>(e))
{
   out << 'A';
   out << e;
} else if (dynamic_cast<B>(e))
{
  out << 'B';
  out << e;
}

and then switch/casing to read like this:

    char t;
    Element *pe;
...
    in >> t;
    switch (t)
    {
      case 'A':
        pe = new A;
        break;
      case 'B':
        pe = new B;
        break;
    }
    in >> *pe;

but it seems inelegant.

What is a better way to stream my disparate objects?


Solution

  • In essence, that’s what any serialization solution will boil down to. Elegance may be improved a bit though but using code generation may still be better (serialization frameworks do that).

    The dynamic cast can definitely be avoided using a virtual function or a map (type_index to tag 1). The switch can be replaced with a map (tag to factory) as well. It is even possible (with some template magic) to use the same code to initialize both maps, like:

    using Factory = void(*)();
    struct SerializationInfo {
      char key;
      type_index type;
      Factory factory;
    };
    
    template <class T>
    SerializationInfo Serializable(char key) // note that SerializationInfo is not a template!
    {
        return {key, typeid(T), []() { return new T(); }}; // IIRC captureless lambda is convertible to a function pointer
    }
    
    Maps buildSerializationMaps(initializer_list<SerializationInfo>);
    
    buildSerializationMaps({
        Serializable<A>('A'),
        Serializable<B>('B'),
    });
    

    where Serializable is a function template that wraps all the serialization information (key, type id, and factory function) in a standard interface.