Sign in to follow this  
Triglav

switch

Recommended Posts

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.

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
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

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
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?

Share this post


Link to post
Share on other sites
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

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
Cool...Like so:


typedef unsigned long ObjectId;

// base object
class ObjectInterface {
};

// object builder...same as mossmoss' code
class 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 type
template< 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 file

class 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

Share this post


Link to post
Share on other sites
in my previous project I solved this problem in the same way. the part of that project was also embedded Python interpreter, so the type-register code was written as "theme registration" in script. the type registration was also directly connected with initialization of various object modificators (because of simplicity of this example I removed several of them):


def RegisterThemeHall(interface):
theme = wrapper.Theme.Interface(interface)

### TILES
# ...

### STATICS
# ...

### DYNAMICS
### type image_id move_type speed range power hp
theme.DynamicObj(DT.Hero, IDI.HERO.TYPE_01[0], MoveType.WALK, 3.5, 0, 2, 200)
theme.DynamicObj(DT.Hero, IDI.HERO.TYPE_02[0], MoveType.WALK, 3.5, 0, 2, 200)

theme.DynamicObj(DT.Monster, IDI.MONSTER.GREEN[0], MoveType.WALK, 3.0, 200, 2, 75)
theme.DynamicObj(DT.Monster, IDI.MONSTER.MUCILAGE[0], MoveType.WALK, 1.5, 130, 10, 80)
theme.DynamicObj(DT.Monster, IDI.MONSTER.RADIO[0], MoveType.WALK, 1.0, 150, 10, 120)
theme.DynamicObj(DT.Monster, IDI.MONSTER.MAGE[0], MoveType.WALK, 1.0, 200, 15, 150)

theme.DynamicObj(DT.Missile, IDI.MISSILE.PULSE[0], MoveType.FLY, 7.0, 110, 30, 0)
theme.DynamicObj(DT.Missile, IDI.MISSILE.MUCILAGE[0], MoveType.FLY, 5.0, 140, 20, 0)
theme.DynamicObj(DT.Missile, IDI.MISSILE.MAGIC[0], MoveType.FLY, 8.0, 400, 45, 0)
#RegisterThemeHall()


def RegisterThemeStore(interface):
# ...
#RegisterThemeStore()

def RegisterThemeCellar(interface):
# ...
#RegisterThemeCellar()

RegisterFunction = {}
RegisterFunction[0x00] = RegisterThemeCellar
RegisterFunction[0x01] = RegisterThemeStore
RegisterFunction[0x02] = RegisterThemeHall

def Register(theme_pointer, theme_id):
""" Loads current type data.
'data_pointer' is a pointer
to data interface in C/C++ code.
"""

try:
RegisterFunction[theme_id](theme_pointer)
except:
raise
#Register()



I know, ugly, but those times it worked nice for me and the deadline was approaching (I wrote the glue code by hand). now the problem is that the object definition is opened and limited by the 'theme.DynamicObj()' function. using the technique mentioned above, those ugly lines will be changed to something as follows (because I'm lazy that ugly stuff with function array is still there):


def RegisterThemeHall(interface):
theme = wrapper.Theme.Interface(interface)
# ...

### DYNAMICS
theme.DynamicObj(DT.Hero.Type01)
theme.DynamicObj(DT.Hero.Type02)

theme.DynamicObj(DT.Monster.Green)
theme.DynamicObj(DT.Monster.Mucilage)
theme.DynamicObj(DT.Monster.Radio)
theme.DynamicObj(DT.Monster.Mage)

theme.DynamicObj(DT.Missile.Pulse)
theme.DynamicObj(DT.Missile.Mucilage)
theme.DynamicObj(DT.Missile.Magic)
#RegisterThemeHall()



the only reason that I founded this thread is that I was wondering whether it's necessary to register newly added objects. whether there is no technique to bypass the cursed land.

suppose that we are using a scripting language for defining our new objects (than it's opened for extern contributors and fans[smile]). today I'm using Lua, so here is something:


-- Registers all objects defined in "obj_dir" directory
function registerObjectDir(_obj_dir)
local directoryList = io.dircontents(_obj_dir)
for objectFile in directoryList do
registerObject(objectFile)
end
end



you have an idea. but how to write something as about in C++ that only one thing to add new object is to link it with new object compiled in .o file?

Share this post


Link to post
Share on other sites
Quote:
Original post by Triglav
you have an idea. but how to write something as about in C++ that only one thing to add new object is to link it with new object compiled in .o file?


My code above gives an example of how a user would "register" the object without having to store the registration code in a central place. Basically I used this object (para-phrasing):


template<typename TBuilder>
class TBuilderRegistrar {
public:
TBuilderRegistrar() {
ObjectManager::Instance()->RegisterBuilder(TBuilder::ID, new TBuilder());
}
};



Instantiating an object of the above type will automatically register the builder with the object manager. You just have to include the following line in your module (not your header file):


TBuilderRegistrar<MyNewObject4> registerMyNewObject4;

// describe all MyNewObject4 members here:
MyNewObject4::MyNewObject4(...) {
}
....



Thus, someone who wants to add a new object type has to:

1) create a header file that describes their new object (inheriting from your base object)
2) create a simple builder class that matches the builder interface (i.e. inherits from the builder base class)
3) in the source module file (.cpp) instantiate a builder registrar object
4) describe their new object methods

You can macro-ize #3 if you want into something equivalant of:

REGISTER_OBJECT(MyNewObject4);

Regards,
Jeff

Share this post


Link to post
Share on other sites
Quote:
Original post by Triglav
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:

*** Source Snippet Removed ***

I'm finding some nice and robust way. there should be no changes in code, when new object types needs to be added.


Haven't read the whole thread but looking at your code i can't see the virtual constructor idiom being used in your code, you can read more about it here this is how you would implement virtual constructors and how you could use this to eliminate that switch statement:


#include <string>
#include <iostream>

template< typename T >
struct cloneable {

virtual T* clone() const = 0;
virtual ~cloneable() = 0;
};

template< typename T >
cloneable<T>::~cloneable() {}

template< typename T >
struct constructable {

virtual T* create() const = 0;
virtual ~constructable() = 0;
};

template< typename T >
constructable<T>::~constructable() {}

struct object_interface : constructable<object_interface>, cloneable<object_interface> {
//la-la-la
virtual ~object_interface() = 0;
};

object_interface::~object_interface() {}

//concreate type must implement clone & create methods
class object_type0 : object_interface {

int a;

public:
//default constructor
object_type0(int b = 0)
: object_interface(), a(b) {}

object_type0(const object_type0& o)
: object_interface(o), a(o.a) {}

object_type0& operator=(const object_type0& o) {
object_interface::operator=(o);
if(this != &o)
a = o.a;
return *this;
}

object_interface* create() const {
return new object_type0();
}

object_interface* clone() const {
return new object_type0(*this);
}
};

//concreate type must implement clone & create methods
class object_type1 : object_interface {

std::string _s;

public:
//default constructor
object_type1(const std::string& s = std::string())
: object_interface(), _s(s) {}

object_type1(const object_type1& o)
: object_interface(o), _s(o._s) {}

object_type1& operator=(const object_type1& o) {
object_interface::operator=(o);
if(this != &o)
_s = o._s;
return *this;
}

object_interface* create() const {
return new object_type1();
}

object_interface* clone() const {
return new object_type1(*this);
}
};

//example of using virtual constructor idiom
int main() {

object_type0 b;

object_interface* i = b.clone();

object_interface* i2 = i->clone();

object_interface* i3 = i2->clone();

delete i;
delete i2;
delete i3;

return 0;
}



[Edited by - snk_kid on August 9, 2004 3:42:16 AM]

Share this post


Link to post
Share on other sites
Quote:
Original post by rypyr
My code above gives an example of how a user would "register" the object without having to store the registration code in a central place.


ah, thank you, I was blind

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this