class ObjectInterface
{
public:
virtual ~ObjectInterface() = 0;
// ...
}; // class ObjectInterface
class ObjectType0 : public ObjectInterface
{
public:
explicit ObjectType0(long _init_data);
// ...
}; // class ObjectType0
class ObjectType1 : public ObjectInterface
{
public:
ObjectType1(long _init_data0, long _init_data1);
// ...
}; // class ObjectType1
class ObjectManager
{
enum ObjectType
{
Type0 = 0,
Type1,
TypeNum,
};
public:
// ...
void readObject(std::istream & _input);
// ...
private:
typedef std::list<ObjectInterface *> ObjectList;
ObjectList objectList;
}; // class ObjectManager
void ObjectManager::readObject(std::istream & _input)
{
ObjectType objectType;
_input >> objectType;
std::auto_ptr<ObjectInterface> newObject();
switch(objectType)
{
case Type0:
{
long initData;
_input >> initData;
newObject.reset(new ObjectType0(initData));
}
break;
case Type1:
{
long initData0;
long initData1;
_input >> initData0 >> initData1;
newObject.reset(new ObjectType1(initData0, initData1));
}
break;
default:
throw std::logic_error("Undefined object type");
}
objectList.push_front(newObject.release());
}
hi, I read some nice stuffs about virtual copy-constructors in Scott Meyers book "More Effective C++". now I'm wondering whether there is any technique to eliminate the switch statement (or function pointers) in following example:
I'm finding some nice and robust way. there should be no changes in code, when new object types needs to be added.
This probably isn't the best way, but here's something that comes to mind. ObjectBuilder and its descendants are a parallel hierarchy of classes:
Next, make a mapping from the ObjectType enum to ObjectBuilder instances.
Finally, change your readObject function to this:
You ask that "there should be no changes in code when new object types need to be added." Well, in what I've provided above, you won't need to change readObject(), but you'll need a parallel class and to ensure you update the registry.
There are some things you can do to improve on this.
Give each class derived from ObjectInterface an explicit constructor taking the input stream. Let the class itself pull what it needs from the input stream, rather than having outside code parse the stream and call a constructor. Then, your parallel classes become much simpler:
And in fact, you can simplify the builders that way by doing this:
Having the actual Objects handle their own construction from stream makes it much easier to build generic code, and so have less to change when you add new classes.
You could probably also have an easier, automated way to build the registry. (Perhaps during construction of the concrete object builders?)
There's probably a number of other ways, better than this, to accomplish things. I was trying to think of some other template methods, but since your type code comes in from a stream at runtime, that eliminates certain options. But maybe some of the above will give you some ideas.
class ObjectBuilder {public: virtual ObjectInterface* make(std::istream& input);}; class ObjectType0Builder : public ObjectBuilder { virtual ObjectInterface* make(std::istream& input) { long initData; input >> initData; return new ObjectType0(initData); }}; class ObjectType1Builder : public ObjectBuilder { virtual ObjectInterface* make(std::istream& input) { long initData0, initData1; input >> initData0 >> initData1; return new ObjectType1(initData0, initData1); }};
Next, make a mapping from the ObjectType enum to ObjectBuilder instances.
std::map<ObjectType, ObjectBuilder*> registry;registry[Type0] = new ObjectType0Builder;registry[Type1] = new ObjectType1Builder;// register other types
Finally, change your readObject function to this:
void ObjectManager::readObject(std::istream& input){ ObjectType objectType; input >> objectType; objectList.push_front( registry[objectType].make(input) );}
You ask that "there should be no changes in code when new object types need to be added." Well, in what I've provided above, you won't need to change readObject(), but you'll need a parallel class and to ensure you update the registry.
There are some things you can do to improve on this.
Give each class derived from ObjectInterface an explicit constructor taking the input stream. Let the class itself pull what it needs from the input stream, rather than having outside code parse the stream and call a constructor. Then, your parallel classes become much simpler:
class ObjectType0Builder : public ObjectBuilder { virtual ObjectInterface* make(std::istream& input) { return new ObjectType0(input); }};class ObjectType1Builder : public ObjectBuilder { virtual ObjectInterface* make(std::istream& input) { return new ObjectType1(input); }};
And in fact, you can simplify the builders that way by doing this:
template <class T>class ConcreteObjectBuilder : public ObjectBuilder { virtual ObjectInterface* make(std::istream& input) { return new T(input); }};std::map<ObjectType, ObjectBuilder*> registry;registry[Type0] = new ConcreteObjectBuilder<ObjectType0>;registry[Type1] = new ConcreteObjectBuilder<ObjectType1>;
Having the actual Objects handle their own construction from stream makes it much easier to build generic code, and so have less to change when you add new classes.
You could probably also have an easier, automated way to build the registry. (Perhaps during construction of the concrete object builders?)
There's probably a number of other ways, better than this, to accomplish things. I was trying to think of some other template methods, but since your type code comes in from a stream at runtime, that eliminates certain options. But maybe some of the above will give you some ideas.
nope, your solution is actually the same as my. the only difference is in amount of object stuff. it is still necessary to edit and recompile the old code.
This is something I've thought a bit about before too and I've come to conclude that there is no easy way to do it.
This is because the new values you are introducing will impact not only the enum (which will cause recompile) but the decision that the compiler makes upon reading that value. The decision-making can be deferred to another object, but still that object will suffer from a recompile when the new object ID is added. Whoever that ultimate decision maker is before it forks it off to the various subclasses will be forced to recompile.
The only way I can think of doing something like this is to get funky and assign unique IDs to each object, then have DLLs labeled object0.dll, object1.dll, .... then depending on the read-in value dynamically load the corresponding DLL and call some predefined factory method that must be there in the DLL.
Regards,
Jeff
This is because the new values you are introducing will impact not only the enum (which will cause recompile) but the decision that the compiler makes upon reading that value. The decision-making can be deferred to another object, but still that object will suffer from a recompile when the new object ID is added. Whoever that ultimate decision maker is before it forks it off to the various subclasses will be forced to recompile.
The only way I can think of doing something like this is to get funky and assign unique IDs to each object, then have DLLs labeled object0.dll, object1.dll, .... then depending on the read-in value dynamically load the corresponding DLL and call some predefined factory method that must be there in the DLL.
Regards,
Jeff
Quote:Original post by Triglav
nope, your solution is actually the same as my. the only difference is in amount of object stuff. it is still necessary to edit and recompile the old code.
No "old" code has to be edited or recompiled if each object type is assumed to be able to come up with a unique key on its own. Everything can be encapsulated within the object itself, so that the object manager never has to change.
Quote:Original post by JohnBolton
No "old" code has to be edited or recompiled if each object type is assumed to be able to come up with a unique key on its own. Everything can be encapsulated within the object itself, so that the object manager never has to change.
Interesting. Can you clarify how this would work exactly?
What he wants is the equivalent of the ability to discover new types at runtime. Thus it rules out all compile-time, templated solutions, including mossmoss'. Again, the only way I can think of accomplishing this is using the dynamic loading of library technique I described above.
Regards,
Jeff
Regards,
Jeff
I didn't claim that my solution was "coding-free". Of course it isn't. What I was going for was a bit of an abstraction, to try and find ways to minimize the changes. Additionally, since your post was titled "switch", my impression was that the switch statement was your primary concern. I was wrong, my apologies.
You can do what you want, but it's far beyond the scope of a simple fix. The library loading idea from rypyr is one way -- a plugin architecture.
Another way would be to build up your own runtime type system (the builtin RTTI would probably be insufficient). And when you do this, you're essentially building a dynamic scripting language.
You can do what you want, but it's far beyond the scope of a simple fix. The library loading idea from rypyr is one way -- a plugin architecture.
Another way would be to build up your own runtime type system (the builtin RTTI would probably be insufficient). And when you do this, you're essentially building a dynamic scripting language.
Quote:Original post by rypyrQuote:Original post by JohnBolton
No "old" code has to be edited or recompiled if each object type is assumed to be able to come up with a unique key on its own. Everything can be encapsulated within the object itself, so that the object manager never has to change.
Interesting. Can you clarify how this would work exactly?
Well all the code necessary was provided by mossmoss. There are four parts, the manager, the key, the registration code, and the object.
The manager never has to be recompiled because all it takes to dispatch is read the key, use the key to find the object in the map, and then call that object's read function. All the functions for registering, unregistering, and dispatching the objects are independent of the number and types of objects. The manager doesn't care about the values or the types of the objects.
The key for an object can also be independent of the manager and other objects. Each object has its key defined by the object. Again, since there is no central manager for key values in the code, it is up to the programmer to make sure they are unique. That's exactly what GUIDs are used for.
Somewhere, the manager has to be told to register an object. This could be done in a central place and that would mean recompiling that code every time there is a new object. That wouldn't be too bad, but if you want to avoid it, you simply call the registration function inside a statically alloated object's constructor or (as you suggested) in a DLL's main. Again, no need for the manager or any other object to know about a new object.
The part that is shared between all objects is the base class and that is independent of the objects that are derived from it.
So there you have it... A system where you can add objects that are dispatched and dispatcher and all the objects are completely independent.
Cool...Like so:
Now in my new object module:
ObjectType0.h:
and finally in the source module:
ObjectType0.cpp:
I like it!
Regards,
Jeff
typedef unsigned long ObjectId;// base objectclass ObjectInterface {};// object builder...same as mossmoss' codeclass ObjectBuilder {public: virtual ObjectInterface* make(std::istream& input) = 0;};class ObjectManager {public: // assume this is a singleton ObjectManager* Instance(); void registerObject(ObjectId id, ObjectBuilder* builder); // add to map here};// "lightweight" object that adds the new object typetemplate< typename BuilderType >class ObjectAdder {public: ObjectAdder() { ObjectManager::Instance()->registerObject(BuilderType::id, new BuilderType()); }};
Now in my new object module:
ObjectType0.h:
#include <ObjectBase.h> // above fileclass ObjectType0 : public ObjectInterface{ ...};class ObjectType0Builder : public ObjectBuilder {public: // unique ID static const ObjectId id = 0; virtual ObjectInterface* make(std::istream& input);};
and finally in the source module:
ObjectType0.cpp:
#include <ObjectType0.h>// this registers the object :)ObjectAdder<ObjectType0Builder> addThisObject;ObjectInterface* ObjectType0Builder::make(std::istream& input){ // do whatever}
I like it!
Regards,
Jeff
This topic is closed to new replies.
Advertisement
Popular Topics
Advertisement