Topic: Proposal: Multiple Type qualification
Author: fjh@cs.mu.OZ.AU (Fergus Henderson)
Date: 2000/03/27 Raw View
Pete Becker <petebecker@acm.org> writes:
>Nir Arbel wrote:
>>
>> 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.
>
>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.
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<Object *> registered_objects;
void register_object(Object *object) {
registered_objects.push_back(object);
}
void draw_objects() {
for (vector<Object *>::iterator i = registered_objects.begin();
i != registered_objects.end(); i++)
{
GUI::Drawable *object =
dynamic_cast<GUI::Drawable>(*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<void *>(object_ptr1) ==
dynamic_cast<void *>(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 T1, class T2>
class Both {
private:
T1 *p1;
T2 *p2;
public:
template <class T /* : public T1, public T2 */ >
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<GUI::Drawable, Persistence::Serializable>
DrawableAndSerializable;
vector<DrawableAndSerializable> registered_objects;
void register_object(DrawableAndSerializable object) {
registered_objects.push_back(object);
}
void draw_objects() {
for (vector<DrawableAndSerializable>::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.
--
Fergus Henderson <fjh@cs.mu.oz.au> | "I have always known that the pursuit
WWW: <http://www.cs.mu.oz.au/~fjh> | of excellence is a lethal habit"
PGP: finger fjh@128.250.37.3 | -- the last words of T. S. Garp.
---
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html ]
Author: Nir Arbel <nirarbel@yifan.net>
Date: 2000/03/27 Raw View
Hi Peter,
I agree with your criticism of my StreamReadableWritable example. In that
particular case writing two separate functions would be a much more coherent
design. I do think that it is often necessary to know that an object answers
to more than one interface without it necessarily signifying that the design
is bad. This is something I keep running into all the time as a programmer. A
programmer might want to write a specialized widget container class that
would only accept widgets that are Colorable and Resizable (Say, to be able
to maintain a uniform coloring theme as well as dynamically change the sizes
of contained widgets). Often I've seen this sort of problem solved by
aggregating many commonly required interfaces in one base class, but this is
hard to maintain, and forces all descendants to have an implementation for
each such interface, even if it is just a default one that emits a runtime
error (where a compile time error is, as always, preferable).
I beg to argue that specifying a combination of types as a qualifier is not
only useful, but also a natural extension of the current OO facilities
offered by C++. Types are often used as contracts between components in the
system. When you ask for an object of type A, you know you'd only receive
objects that answer to the contract denoted by A. In my experience, it often
makes sense to ask for an object that answers to more than one contract, and
the optimal solution would be to do just that, rather than modify an existing
contract or create a new hybrid one.
Regards, Nir
Pete Becker wrote:
> Nir Arbel wrote:
> >
> > Hi,
> >
> > 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.
>
> 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.
>
> --
> Pete Becker
> Dinkumware, Ltd. (http://www.dinkumware.com)
> Contibuting Editor, C/C++ Users Journal (http://www.cuj.com)
---
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html ]
Author: Pete Becker <petebecker@acm.org>
Date: 2000/03/29 Raw View
Fergus Henderson wrote:
>
> 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:
>
A more natural choice would be Drawable or Serializable, since by your
hypothesis both of these are bases of any object that you'll be dealing
with.
--
Pete Becker
Dinkumware, Ltd. (http://www.dinkumware.com)
Contibuting Editor, C/C++ Users Journal (http://www.cuj.com)
---
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html ]
Author: Nir Arbel <nirarbel@yifan.net>
Date: 2000/03/24 Raw View
Hi,
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. The StreamReadableWritable, like any other
interface-hybrid, serves the purpose of just one function, or at best,
several functions. Often,
this approach would force us to combine interfaces that are very
difficult to relate to each other. A preferrable solution might be to
ask for an object that simply
qualifies for more than one type:
void Serialize (StreamReadable StreamWritable * object) // arbitrary
syntax
This makes the function's signature easier to read, eliminates the need
to create a hybrid type, and perhaps more importantly, would work for
objects that earlier
on were defined as direct descendents of StreamReadable and
StreamWritable without modifying them to inherit from the hybrid
interface instead.
The problem with implementing this mechanism is that the C++ object
model cannot guarantee that the one pointer passed would work for every
object that
inherits the specified types/interfaces, since these "sub-objects" may
be layed differently in each such descendent. This problem can be
overcome by having
the compiler actually ask for one pointer for each type so that in the
above example, two pointers would be passed: one for the StreamReadable
portion of the passed object, and the other for the StreamWritable
portion of it. This in turn raises the problem of having one moniker to
refer to two pointers. My suggestion in this case is -- have the
compiler guess which actual pointer to use according to how the moniker
is used. For instance:
object->write_to_stream ()
would clearly signify that the moniker should be used as the pointer to
the StreamWritable.
In any case where the form of use is ambiguous, the compiler should emit
an error message.
Since multiple type qualification in this case necessarily dictates
polymorphic behaviour, it only makes sense to use it for
pointers/references.
2. C++ does not at the moment let you specify what type a template
parameter should conform to. Often, it is said that this is a plus,
since it allows you to treat
a type as though at conformed to not just one interface, but several.
And indeed, templates are often used where multiple type qualification
might have been used
as a more elegant solution, as it would've made it clear what interfaces
candidate types/object have to answer to, and would catch errors earlier
on -- as soon
as one tries to instantiate a template with illegal types as parameters.
If multiple type qualification were implemented in C++, it might as well
have been implemented for (optionally) specifying the types of template
parameters, for instance:
template <class T : HasWeight HasColor> // arbitrary syntax
class ObjectSorter
{
void addObject (T * object);
};
This is just my initial thoughts on the subject. Any comments welcome.
Thanks,
Nir Arbel
---
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html ]
Author: Pete Becker <petebecker@acm.org>
Date: 2000/03/25 Raw View
Nir Arbel wrote:
>
> Hi,
>
> 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.
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.
--
Pete Becker
Dinkumware, Ltd. (http://www.dinkumware.com)
Contibuting Editor, C/C++ Users Journal (http://www.cuj.com)
---
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html ]