Here's the situation:
Class definitions:
CDataItem
{
};
CMyDataItem:public CDataItem
{
};
CYourDataItem:public CDataItem
{
};
CDataHandler
{
public:
virtual void op(CDataItem &rItem);
};
CMyDataHandler:public CDataHandler
{
public:
void op(CMyDataItem &rItem);
};
CYourDataHandler:public CDataHandler
{
public:
void op(CYourDataItem &rItem);
};
CProcess
{
public:
BOOL proc(CMyDataItem &rItem);
BOOL proc(CYourDataItem &rItem);
};
Ok, here's what I'd like to do:
Implementations:
CMyDataHandler::op(CMyDataItem &rItem)
{
CDataHandler::op(&rItem);
}
CDataHandler::op(CDataItem &rItem)
{
CProcess procNew;
procNew.proc(rItem);
}
This doesn't work as written, because the CMyDataItem object is cast to a
CDataItem object when I pass it to the CDataHandler::op() call. Is there a
way around this without resorting to run-time checking? If I define the
CDataHandler::op() as a pure virtual, the code in each sub-class has the same
calling conventions, but different parameters, which presents a problem, in
that roughly the same code is in a dozen places.
Any ideas on getting around this problem would be appreciated.
Thanks,
John
How about the following
1. All the Handler op() s have the same signature
class *Handler{ //just used a wildcard for all your handler classes
..
virtual void op (CDataItem& rItem);
..
};
2. Declare a class which will implicitly do all the downcasting for you
class DataVisitor
{
public :
virtual Void Visit(CMyDataItem& rItem) { _myPtr = &rItem;}
virtual Void Visit(CYourDataItem& rItem) { _yourPtr = &rItem;}
void getPointer(CMyDataItem*& myPtr) { myPtr = _myPtr; }
void getPointer(CYourDataItem*& myPtr) { yourPtr = _yourPtr; }
private :
CMyDataItem* _myPtr; // initialise to 0 in the constructor
CYourDataItem* _yourPtr;// initialise to 0 in the constructor
};
3. Add the following methods to your DataItem classes
e.g. for the CMyDataItem class
void CMyDataItem::Visit(DataVisitor& dv)
{
dv.Visit(*this);
}
CMyDataItem* CMyDataItem::downcast(CDataItem& rItem) // this is a STATIC operation
{
CMyDataItem* rItemPtr;
DataVisitor dv;
rItem.Visit(dv);
dv.getPointer(rItemPtr);
return rItemPtr;
}
4. Your Handlers could now do the downcasting
CMyDataHandler::op(CDataItem& rItem)
{
CProcess procNew;
CMyDataItem* myPtr = CMyDataItem::downcast(rItem);
if ( myPtr )
{
procNew.proc(*myPtr);
}
}
This solution (as is obvious) is based on the Visitor pattern. It is also very similar to and based upon
a solution that Robert Martin posted on the net some time back (had something to do with Fruit and Apples)
The overhead that you will need to incur is that whenever you do add/remove new types of DataItems (e.g.
CTheirDataItem) you will need to add/remove
1. Pointer Attribute in the DataVisitor class i.e. CTheirDataItem* _theirPtr;
2. Visit operation in the DataVisitor class i.e. DataVisitor::Visit(CTheirDataItem&)
3. getPointer operation in the DataVisitor class i.e. DataVisitor::getPointer(CTheirDataItem*&)
Also each such data item will need to have two operations defined for it
i.e.
CTheirDataItem::downcast() and CTheirDataItem::Visit()
Some of these will lend themselves to easy "templatization" e.g. the DataHandlers
Note that to make matters explicit I called the operation "downcast()", though there is no explicit
downcasting involved.
Any comments / suggestions are most welcome
Dhananjay
------------------------------------------------------------
Dhananjay Nene | I speak for myself and not
dhananj...@att.com | necessarily for my employer
------------------------------------------------------------
You have discovered the myths of contravariance. There is no better
example to it than you gave. One solution to this is, that either your
CDataHandler has no 'op', therefor you can define it in each subclass just
like you want. Another solution is not to redefine 'op' in the subclasses.
According to the example (if it is not drastically simplified) this may be
a way to do it.
However, if you want more reading, read about the Liskov(sp?) Substitution
Principle, which may give more insight on this:
The interface to CDataHandler says, that every CDataHandler-Object accepts
a CDataItem as a parameter to object. To sacrifice the LSP every subclass
has to conform to this interface. You don't want them to do so, therefor
you violate the LSP.
Sometimes this is inherent to a solution, but usually it is possible to
redesign.
Another text I'd like to point you to is written by Robert Martin, who
frequently writes here (comp.object). I believe that his "Principles of
OOD" is placed somewhere on his Webserver: http://www.oma.com. If you
can't find it there, email me.
You may also read about the visitor pattern in "Design Patterns" (Gamma et
al.)
Regards,
Olaf
--
Who is General Failure and why is he reading my disk?
[lots of code snipped]
>
> This doesn't work as written, because the CMyDataItem object is cast to a
> CDataItem object when I pass it to the CDataHandler::op() call. Is there a
> way around this without resorting to run-time checking? If I define the
> CDataHandler::op() as a pure virtual, the code in each sub-class has the same
> calling conventions, but different parameters, which presents a problem, in
> that roughly the same code is in a dozen places.
>
What you have designed is a standard dual inheritance hierarchy. Two
separate inheritance hierarchies whose derived classes are related
in a strict protocol. i.e. given two base classes B1 and B2 where there
is a relationships between B1 and B2. (i.e. a virtual function in B1 that
takes a B2). For each derivative (D1x) of B1 there is a corresponding
derivative (D2x) of B2. The relationship between D1x and D2x is the same
as the relationships between B1 and B2, except that the types involved in
the relationship *must* be D1x and D2x. (i.e. the virtual function in D1x
must take a D2x argument).
There is no general solution to this problem that does not resort to
run time type checking. The type information needed by the 'x'
derivatives is lost when passed through the base class interface. It
can only be regained through run time type checking.
However, this is not as bad as it sounds. The 'x' derivatives *know*
the types that they are supposed to deal with. They simply assert
that the types really are what they expect.
I am presenting a paper on this topic at COOTS this year. If anybody would
like a copy of that paper, let me know. It is entitled "Dual Inheritance
Hierarchies".... IF there are enough requests, I'll put the
paper up on my web site.
--
Robert Martin | Design Consulting | Training courses offered:
Object Mentor Assoc.| rma...@oma.com | OOA/D, C++, Advanced OO
14619 N. Somerset Cr| Tel: (847) 918-1004 | Mgt. Overview of OOT
Green Oaks IL 60048 | Fax: (847) 918-1023 | http://www.oma.com
Case O.Classtype = 'D2x' do
(D2x)(O) --cast to D2x and call relevant func
Case O.Classtype = 'D2y' do etc.
This case statement becomes unwieldly and just looks wrong in the OO
world ! My most recent improvement is to dynamically build up an array of
pointers to all the B1 objects as they get initialised, i.e. they
"register" themselves (and the classname each one is responsible for)
when they get created. Then instead of the Case statment I dynamically
compare the O.Classname and thus find the pointer to the object who is
supposed to handle this type.
While I still dont consider it particularly elegant or high performing,
at least its more maintainable. I'm in the process of conducting
performance tests and would very much like your comment and INDEED your
paper !
Thanks.
[code with visitor and downcasting removed]
The very idea with visitor pattern is to get rid of downcasting,
so another downcastingfree visitor implementation is of course
something like this:
#include <stdio.h>
#define BOOL int
class item {
public:
virtual void accept(class visitor & v) = 0;
};
class visitor {
public:
virtual void visit (class myItem & theItem) = 0;
};
class myItem: public item {
public:
void accept(class visitor & v) { v.visit(*this);}
};
class handler {
public:
void op (class item & rItem, class visitor & funcPtr) {
rItem.accept(funcPtr);
}
};
class myHandler: public handler {
public:
void op (class myItem & rItem, class visitor & funcPtr) {
handler::op(rItem, funcPtr);
}
};
class process: public visitor {
public:
BOOL retVal;
void visit (myItem & theItem); // renamed proc() -> visit()
};
void process::visit (myItem & theItem) {
printf("Hello! %s %d\n", __FILE__,__LINE__);
retVal = 0;
}
main() {
myHandler* mh = new myHandler;
process* pv = new process;
myItem* mi = new myItem;
mh->op(*mi, *pv);
}
> CDataItem
> {
> };
>
> CMyDataItem:public CDataItem
> {
> };
>
> CYourDataItem:public CDataItem
> {
> };
>
> CDataHandler
> {
> public:
> virtual void op(CDataItem &rItem);
> };
>
> CMyDataHandler:public CDataHandler
> {
> public:
> void op(CMyDataItem &rItem);
> };
>
> CYourDataHandler:public CDataHandler
> {
> public:
> void op(CYourDataItem &rItem);
> };
>
> CProcess
> {
> public:
> BOOL proc(CMyDataItem &rItem);
> BOOL proc(CYourDataItem &rItem);
> };
>
> Ok, here's what I'd like to do:
>
> Implementations:
>
> CMyDataHandler::op(CMyDataItem &rItem)
> {
> CDataHandler::op(&rItem);
> }
>
> CDataHandler::op(CDataItem &rItem)
> {
> CProcess procNew;
> procNew.proc(rItem);
> }
>
> This doesn't work as written, because the CMyDataItem object is cast to a
> CDataItem object when I pass it to the CDataHandler::op() call. Is there a
> way around this without resorting to run-time checking? If I define the
> CDataHandler::op() as a pure virtual, the code in each sub-class has the same
> calling conventions, but different parameters, which presents a problem, in
> that roughly the same code is in a dozen places.
In other words, you need to dispatch both on the DataItem and on the Handler
type. A good solution is described as the Visitor design pattern in
"Design Patterns - Elements of Reusable Object-Oriented Software" Gamma/Helm/
Johnson/Vlissides, Addison-Wesley 1995, ISBN 0-201-63361-2
--
Thomas Maeder (mae...@glue.ch)
GLUE Software Engineering AG, Zieglerstr. 34, CH-3007 Bern, Switzerland
Phone: (++41) 31 385 30 11 Fax: (++41) 31 385 30 18
I disagree. The idea with 'Visitor' is to find a way to add
new operations to an existing hierarchy, without changing that
hierarchy. This requires dynamic type resolution, because the
base type of the hierarchy must be resolved to the actual run time
type of the object being visited. And this is the definition
of a type safe downcast.
YE GODS! OK, OK, OK! There have been dozens and dozens of requests for
this paper. So I have made it available on my web site. Just click
on the "Freeware by Email" button. You'll see it in the list of
options.
Thats the way it is described in the book "Design Patterns". However
in this particular situation I believe it is used for
a different purpose i.e. To have the ability to extend the dual class
hierarchy (when one needs to add derived classes to each of the legs of
the dual class hierarchy) with minimal impact on the existing classes.
So rather than facilitating operations being added to an existing
hierarchy, the pattern is facilitating the hierarchy to be extended for the
existing operation(s).
Also some of the consequences as listed under the Visitor Pattern seem to
change in this situation.
e.g. Consequences as per the book
1. Visitor makes adding new operations easy
2. Adding new ConcreteElement classes is hard
now become
1. Visitor makes adding new derived classes easy
2. Adding new operations to the class hierarchy is hard.
Under this context the interpretation that the pattern is used for downcasting
does not seem to be far-fetched.
I wonder whether these apparently orthogonal purposes are in some way the
same. If yes, could someone help me understand why (I've tried hard but
failed so far.). If no, do Gamma et. al. need to add an additional "Intent"
in their book for the Visitor Pattern (:-)) ?
Dhananjay Nene
Consider that a visitor class is nothing more than a typecase.
That is:
class MungeVisitor
{
public:
MungeX(X&);
MungeY(Y&);
MungeZ(Z&);
};
(with its associated visitor bindings) is logically equivalent to:
if (typeid(o) == typeid(X)) MungeX(static_cast<X>(o))
else if (typeid(o) == typeid(Y)) MungeY(static_cast<Y>(o))
else if (typeid(o) == typeid(Z)) MungeZ(static_cast<Z>(o))
Thus, the Visitor pattern IS RTTI (Run Time Type Information) and can be
employed wherever RTTI is needed.
One place RTTI is extremely useful is for binding dual inheritance hierarchies.
See my paper (Design Patterns for Dealing with Dual Inhertance Hierarchies) which
you can get from the freeware section of my web site.
> If no, do Gamma et. al. need to add an additional "Intent"
> in their book for the Visitor Pattern (:-)) ?
I have created a new pattern called "Rungs of a Dual Hierarchy" that discusses
this issue. It is one of the patterns from the above paper.
maeder> In other words, you need to dispatch both on the DataItem and on
maeder> the Handler type. A good solution is described as the Visitor
maeder> design pattern in "Design Patterns - Elements of Reusable
maeder> Object-Oriented Software" Gamma/Helm/ Johnson/Vlissides,
maeder> Addison-Wesley 1995, ISBN 0-201-63361-2
Is that a "good solution" to a _design_ problem?
To me it seems rather a coding kludge to get partially around some
limitation of the language...