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?
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.