TITLE: multiple type qualification (Newsgroups: comp.std.c++, 27 Mar 2000) ARBEL: Nir Arbel >> 1. Often, when I do OOP programming, I write a function that needs to >> work on an object that answers to more than one interface, or type. For >> instance: function Serialize() may want an object that has both >> WritableToStream and ReadableFromStream, in which case the usual >> solution is to create a hybrid type: >> >> class StreamReadableWritable : public WritableToStream, public >> ReadableFromStream ... >> >> However, this is kludge. BECKER: Pete Becker > Agreed. Seems to me that the right answer is to write two functions, one > for reading and one for writing. Those are distinct operations, with > rather different code. The design problem is in combining them into a > single function in the first place, not in the difficulty of writing > that function. HENDERSON: fjh@cs.mu.OZ.AU (Fergus Henderson) Hmm... I think you may have misunderstood what Nir Arbel was trying to say. In the design sketched above, I think reading and writing were indeed separate virtual functions. Perhaps the name `Serialize' was poorly chosen, since normally a Serialize() function would write the object out, and you would use a DeSerialize() function to read the object back in. So the example would be better if the function was just called `do_some_io()'. Anyway, let me see if I can explain what I think Nir Arbel was getting at, by way of a similar but more elaborate example, and then I will suggest a solution using standard C++, outline its drawbacks, and explain why I think it is probably not worth extending C++ with more direct for this. Suppose I have two libraries, each of which provides an abstract base class -- for example, `Drawable' (from the GUI library) and `Serializable' (from the Persistence library): // gui.h namespace GUI { class Drawable { public: virtual void draw() = 0; ... }; } // persistence.h namespace Persistence { class Serializable { public: virtual void serialize(ostream &) = 0; virtual void deserialize(istream &) = 0; ... }; } Next suppose that there is a third library Widget, which defines some objects which are both Drawable and Serializable, // widget.h #include "gui.h" #include "persistence.h" class Widget : public GUI::Drawable, public Persistence::Serializable { ... }; class EditorWidget: public Widget { ... }; class GraphWidget: public Widget { ... }; ... and a fourth library Shape, which does likewise: // shape.h #include "gui.h" #include "persistence.h" class Shape : public GUI::Drawable, public Persistence::Serializable { ... }; class Square : public Shape { ... }; class Circle : public Shape { ... }; ... Now, suppose I'm writing an application framework which depends on the first two libraries, and has a function called say register_object() which takes a parameter and stores it in a data structure from which the code will later extract it and then call methods of both the GUI::Drawable and the Persistence::Serializable classes. Furthermore, I want the function to work for both Widgets and for Shapes. How should I declare this function? One approach would be to create a new class DrawableAndSerializable which is derived from both of the abstract base classes: class DrawableAndSerializable : public GUI::Drawable, public Persistence::Serializable {}; and to then declare the function to take a pointer or reference to this class: void register_object(DrawableAndSerializable *object); However, this approach doesn't work, at least not without modifying the source code of the Shape and Widget libraries, since the Shape and Widget classes are not derived from DrawableAndSerializable. Another approach which sometimes works is to make the function in question a template, as Nir Arbel mentioned. However in this case using templates won't work, since the register_object() function needs to store its parameter in a data structure, and we want a single collection of all registered objects, not a different collection for each different type of object register. Another alternative is to pass the parameter as an pointer to some root class `Object' and use dynamic_cast when you want to call methods: class Object { virtual void ~Object() {} }; vector registered_objects; void register_object(Object *object) { registered_objects.push_back(object); } void draw_objects() { for (vector::iterator i = registered_objects.begin(); i != registered_objects.end(); i++) { GUI::Drawable *object = dynamic_cast(*i); object->draw(); } } But this only works if the GUI::Drawable and Persistence:Serializable classes both derive from some common base class `Object', and since they come from different libraries, and since the C++ standard library does not define any such base class, that is unlikely. Furthermore, you lose compile-time type safety. An alternative which does work is to pass the same parameter twice, with different types each time: void register_object(GUI::Drawable *object_ptr1, Persistence::Serializable *object_ptr2) { // check that both pointers point to the same object assert(dynamic_cast(object_ptr1) == dynamic_cast(object_ptr2)); ... } But this is ugly, and does not give proper static checking either; there is no compile-time guarantee that the two parameters will point to the same object. Finally, here's a solution that does give you proper static checking: template class Both { private: T1 *p1; T2 *p2; public: template Both(T *p) : p1(p), p2(p) {} T1 *as_first() { return p1; } T2 *as_second() { return p2; } operator T1 *() { return p1; } operator T2 *() { return p2; } }; typedef Both DrawableAndSerializable; vector registered_objects; void register_object(DrawableAndSerializable object) { registered_objects.push_back(object); } void draw_objects() { for (vector::iterator i = registered_objects.begin(); i != registered_objects.end(); i++) { GUI::Drawable *drawable_object = *i; drawable_object->draw(); // or alternatively, instead of the two lines above: // (*i).as_first()->draw(); } } This solution is quite nice, IMHO. As far as the user is concerned, this template `Both' class acts pretty much like the extension that Nir Arbel was asking for. The only significant drawback is the occaisional need to insert additional local variables (such as `drawable_object' above) or calls to as_first() or as_second(). But this does not seem like a particularly difficult burden. Note that direct compiler support is unlikely to lead to a significantly more efficient solution, because the compiler would have to either keep two pointers or use dynamic casts under the hood (depending on whether they choose to optimize for time or space). So given this, and given also that the circumstances in which this feature is needed are fairly rare, I don't see any pressing need to extend C++ along these lines. _______________________________________________ cpptips mailing list http://cpptips.hyperformix.com