switch

Started by
12 comments, last by Triglav 19 years, 8 months ago
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:

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());
}

I'm finding some nice and robust way. there should be no changes in code, when new object types needs to be added.
Triglav - Member of TAJGA Team
Advertisement
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:

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.
Triglav - Member of TAJGA Team
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
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.
John BoltonLocomotive Games (THQ)Current Project: Destroy All Humans (Wii). IN STORES NOW!
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
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.
Quote:Original post by rypyr
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?


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.
John BoltonLocomotive Games (THQ)Current Project: Destroy All Humans (Wii). IN STORES NOW!
Cool...Like so:

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