Topic: RTTI with inheritance: switch (object)


Author: kanze@us-es.sel.de (James Kanze)
Date: 25 May 93 12:13:55
Raw View
In article <C7AJs8.8F7@apollo.hp.com> vinoski@apollo.hp.com (Steve
Vinoski) writes:

|> In article <KANZE.93May17182403@slsvdnt.us-es.sel.de> kanze@us-es.sel.de (James Kanze) writes:
|> > void
|> > function( Base* object )
|> > {
|> >  try
|> >  {
|> >   throw *object ;
|> >  }
|> >  catch( Foo )
|> >  {
|> >   // ...
|> >  }
|> >  catch( Bar )
|> >  {
|> >   // ...
|> >  }
|> >  catch( ... )
|> >  {
|> >   // ...
|> >  }
|> > }
|> >
|> >Of course, this isn't as pretty.  More important, since exceptions are
|> >meant for exceptional cases, the compiler writer will supposedly not
|> >make them very efficient.

|> I don't believe this will work as intended.

|> Section 15.2, page 356 of the ARM states that "a throw-expression
|> initializes a temporary object of the static type of the operand of
|> throw and uses that temporary to initialize the appropriately-typed
|> variable named in the handler."  In your example, *object is
|> statically typed as a Base, so the catch(...) clause will *always* be
|> entered, even if object actually points to a Foo or Bar (which are
|> both derived from Base in the original example).

|> For what it's worth, the implementation of exceptions in the HP C++
|> compiler backs me up.

You're right.

Just put it down to my lack of experience with exceptions.  While it
might be nice for the language to work as I expected it, on thinking
it over, I can see why it doesn't.  After all, how is the compiler to
figure out which copy constructor to call, since constructors cannot
be virtual.  And how can it possibly know how big the static object
is, to reserve the memory.
--
James Kanze                             email: kanze@us-es.sel.de
GABI Software, Sarl., 8 rue du Faisan, F-67000 Strasbourg, France
Conseils en informatique industrielle --
                   -- Beratung in industrieller Datenverarbeitung




Author: fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON)
Date: Sat, 22 May 1993 10:59:52 GMT
Raw View
maxtal@physics.su.OZ.AU (John Max Skaller) writes:

>Thats why variants are superior to discriminated unions:
>if you have variants and classes, you can have discriminated
>unions too.
>
>This is a good thing: you cant change the 'type' of a variant,
>because a variant is a reference to an *actual* single normal
>type. So you cant do a 'type changing assignment' to a variant.
>
>But you *can* to a dunion. And there is a recently discovered
>trick that allows you to use a variant (whose type cannot
>be changed after initialisation) to create a discriminated
>union (whose type *can* be changed by assignment after initialisation).
>
>If it wasnt for this fact, variants would not be so useful.
>I'll leave it to the inventor, Fergus Henderson, to
>explain how this is done.

The reason that the trick works is that there is something that
variants *don't* commute with: membership of a struct/class.
A struct containing a variant is not the same as a variant of structs.
So if I have
 extern variant [int, String] foo();
 struct S {
   variant [int, String] member;
   S(int i) : member (i) {}
   S(String j) : member (j) {}
 };
 S bar = foo(), baz = foo();
then bar and baz do have the same type, even though their variant
members bar.member and baz.member may at runtime actually have different
types.
[Why is it that variants don't commute with composition (struct/class
membership)? Well, my answer is: so that the trick works, of course ;-).
Maybe Max has a better answer to this question.]

Next question: what happens if we assign S's?
 bar = baz;
Well, we haven't defined an assignment operator, so it should default
to memberwise assignment.  I *think* that there is nothing special
about memberwise assignment for variant members, in which case memberwise
assignment would be just the same as
 bar.member = baz.member
since there is only one member, which in turn is just the same as
 select(bar.member) {
   type(int& i, int &j)  { i = j; }
   type(String& i, int& j) { i = j; } // illegal?
   type(int& i, String &j) { i = j; } // illegal?
   type(String& i, String& j) { i = j; }
 }
and this would not do what we want, indeed it will be illegal unless
there is a conversion to/from String to int.

*Perhaps* it would be better to make memberwise assignment for
variant members actually be type-changing reinitialization. (Comments, Max?)
But in any case, we can achieve the same effect with an extra
level of indirection:

 variant My_Variant [int, double, String];

 class My_Dunion {
   struct Struct_Containing_Variant {
     My_Variant val;
     Struct_Containing_Variant (My_Variant v) : val(v) {}
   } *ptr;
 public:
   My_Dunion (const My_Variant & v)
  : ptr = new Struct_Containing_Variant (v) {}
   My_Dunion (const My_Dunion & v)
  : ptr = new Struct_Containing_Variant (v.ptr->val) {}
   My_Dunion& operator = (const My_Variant & v)
  { delete ptr; ptr = new Struct_Containing_Variant (v); }
   ~My_Dunion ()
  { delete ptr; }

   My_Variant & value()
  { return ptr->val; }
 };

The actual storage for the variant value is dynamically allocated, and
assignment is implemented by deleting the old value and allocating a new
value. We provide a member function value() which returns a reference
to the value so that users of My_Dunion can do their own type
switches. Eg.

 int is_int (My_Dunion &x) {
   select (x.value()) {
     type (int)   { return TRUE; }
     type (...)  { return FALSE; }
   }
 }

In general this is still a moderately primitive data type, though -
it would might be best to further encapsulate the operations on
this type so that all the "select"s are in the one place, as
Robert Martin has suggested. However, the advantage of keeping
things simple is that it allows us to generalize: since My_Dunion
doesn't depend on the details of My_Variant, we can make Dunion a
template class:
 template <class Variant>
 class Dunion { ... };

As an example, here's a simple 'mixed' data type which is either an
int, a double, or a string.

 #include <dunion.h> // template dunion class, as above

 class Mixed {
   variant TheVariant [int, double, String];
   Dunion<TheVariant> val;
 public:
   Mixed(TheVariant x) : val(x) {}
   friend Mixed operator+ (Mixed, Mixed);
   // ... other member/friend functions as desired
 };

 Mixed operator+ (Mixed x1, Mixed x2) {
   variant Numeric [int, double];
   select(x.value(), x2.value()) {
     type (Numeric xv, Numeric yv) { return xv + yv; }
     type (String xv, String yv) { return concatenate(xv,yc); }
     type (Numeric, ...) { raise SomeException; }
     type (String, ...) { raise SomeException; }
   }
 }

You could use private inheritance instead of having a member variable 'val',
it makes little difference. You could even use public inheritance, or
equivalently forward the member function value(), if you want to
allow client code to do type switching on Mixed variables.

You might decide to instead of raising an exception, just store an error
value, somewhat like IEEE NaN and Infinity. You could do this by
adding a new type Error to the variant, and having the incorrectly
typed additions return a value of this type rather than raising an exception.

One thing that's annoying about our Dunion template is that it uses
an extra level of indirection, with accompanying dynamic allocation
overheads. We can avoid this, and get the same efficiency properties
as traditional C unions, by using the placement operator new and
explicit call to the destructor:

 template <class Variant>
 class Dunion {
   struct Struct_Containing_Variant {
     Variant val;
     Struct_Containing_Variant (Variant v) : val(v) {}
   } the_val;
 public:
   Dunion (const Variant & v)
  : the_val(v) {}
   Dunion (const Dunion & v)
  : the_val(v.the_val) {}
   Dunion& operator = (const Variant & v)
  { the_val.~Struct_containing_Variant;
    new(&the_val) Struct_Containing_Variant (v); }
   Variant & value()
  { return the_val.val; }
 };

Note that unlike traditional C unions, you *can* have a Dunion containing
a class like String that has constructors/destructors.

--
Fergus Henderson                     This .signature virus might be
fjh@munta.cs.mu.OZ.AU                getting old, but you still can't
                                     consistently believe it unless you
Linux: Choice of a GNU Generation    copy it to your own .signature file!




Author: fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON)
Date: Sat, 22 May 1993 12:14:13 GMT
Raw View
rmartin@rcmcon.com (Robert Martin) writes:

>I see your point.   You don't want the nodes of the parse tree to
>"know" what purpose they will be used for.  So let me suggest another
>route.  Separate the data structure from the intent of the data.
>
>class Node
>{
>  public:
>    virtual void Invoke(NodeProcessor&) const = 0;
>};
>
>class BinaryOpNode : public Node
>{
>  public:
>    virtual void Invoke(NodeProcessor& p) const
>    {p.BinOp(*this);}
>};
>
>class UnaryOpNode : public Node
>{
>  public:
>    virtual void Invoke(NodeProcessor& p) const
>    {p.UnaryOp(*this);}
>};
>
>class NodeProcessor
>{
>  public:
>    virtual void BinOp(const BinaryOpNode&) = 0;
>    virtual void UnaryOp(const UnaryOpNode&) = 0;
>};
>
>class PascalGeneratorNodeProcessor : public NodeProcessor {...};
>class M68KGeneratorNodeProcessor   : public NodeProcessor {...};
>class PostFixPrinterNodeProcessor  : public NodeProcessor {...};
>
>You can derive whatever kind of node processor you like.  You can walk
>the tree in whatever order you desire, and then pass the node
>processor to the Invoke member of each node.

I think that John Skaller has hit the nail on the head when he
said that this is good, but its very long-winded and tedious and
we need a better method. But I think I can elaborate a little
more on the details of how this particular method becomes
long-winded and tedious:

What if I want to modify the tree as I'm going? Well, I guess that
means I need a new TreeNode method
 void Modify(ModifyingNodeProcessor &) = 0;
which is exactly the same as Invoke except non-const.
And I'll have to have UnaryNode::Modify, and BinaryNode::Modify, etc.

What if I want to pass parameters or return values as I walk the tree?
Well, I could add a whole bunch of "Invoke"-like methods to TreeNode
with every possible parameter & return value combination, but that's
obviously not going to work very well.  I guess I have to store these
parameters and return values as state in the NodeProcessor.  This is
going to be a bit ugly and is definitely going to increase the length
of my code, not to mention it's efficiency.

Another drawback is that it requires two virtual function calls for
every time you do a type switch, which might be undesireable if efficiency
is important (as indeed it is in the application that got me
interested in variants, an interpreter for a dynamically typed language.)

Mind you even despite these drawbacks it's a very interesting technique
and I might seriously consider using it (at least until my C++ compiler
supports variants ;-)

--
Fergus Henderson                     This .signature virus might be
fjh@munta.cs.mu.OZ.AU                getting old, but you still can't
                                     consistently believe it unless you
Linux: Choice of a GNU Generation    copy it to your own .signature file!




Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Sat, 22 May 1993 13:49:16 GMT
Raw View
In article <9314220.25606@mulga.cs.mu.OZ.AU> fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON) writes:
>maxtal@physics.su.OZ.AU (John Max Skaller) writes:
>
>The reason that the trick works is that there is something that
>variants *don't* commute with: membership of a struct/class.

 Two things. The other one is function returns.  I think :-)

>[Why is it that variants don't commute with composition (struct/class
>membership)? Well, my answer is: so that the trick works, of course ;-).
>Maybe Max has a better answer to this question.]

 Yes, I think it is beginning to become clear.

 Variants are just type inference:

 variant [long,int] x = 1; // x is an int
 f( variant [long,int] y) { ...}
 // f(long y) { ...}
 // f(int y) { ...}
 f(x); // x is an int: so call f(int)

All this can be done completely statically by type inference
and 'duplicating' variant functions so that existing
overload resolution does the work.

 Now consider

 variant [int, long] f();

 x = f();

No way to do this statically: there *has* to be a type tag associated
with the return value, the function must have enough store reserved
for returning either a long or an int, and the selection has to
be done at run -time.

Ok: the other case: structs.

 struct X {
  variant [int, long] x;
  X(int y);
 };

Well, the implementation of the constructor is hidden. So you
cant tell which type 'x' is statically. So you must reserve
enough space in the struct for either an int or a long,
and also for a discriminating tag.

Its interesting that this is a direct consequence of
encapsulation.
>
>*Perhaps* it would be better to make memberwise assignment for
>variant members actually be type-changing reinitialization. (Comments, Max?)

 There are two reasons not to do this (at least).

 The first reason is that it isnt safe.

 The second reason is its not obvious how to define it.
That is: if you define it as *always* doing a reinitialisation,
then you cannot ever apply a user defined assignment operator.

 But that is absurd, at least for a variant that
is optimised away by type inference: there may not even be
enough storage available to reinitialise it.

>
>The actual storage for the variant value is dynamically allocated, and
>assignment is implemented by deleting the old value and allocating a new
>value.

 Be careful! A pointer to a variant is a variant,
so you have not really escaped the problem, in fact,
I think this wont work: you might actually *need* a non
pointer object that can be destroyed and re-initialised.

>You could use private inheritance instead of having a member variable 'val',
>it makes little difference. You could even use public inheritance, or
>equivalently forward the member function value(), if you want to
>allow client code to do type switching on Mixed variables.

 Careful: variants commute with inheritance I think:
so that inheriting from a variant is distinctly different from
having a variant member object :-)

>
>Note that unlike traditional C unions, you *can* have a Dunion containing
>a class like String that has constructors/destructors.

 Yes. Dunion is a real class. You can even make your
own version, derive from it, polymorphise it, specialise:
do what ever you want.

 If this works, Dunion is actually *better* than
a built in discriminated union type because you can create
your own type using classes and variants to do exactly what you want.


--
        JOHN (MAX) SKALLER,         INTERNET:maxtal@suphys.physics.su.oz.au
 Maxtal Pty Ltd,      CSERVE:10236.1703
        6 MacKay St ASHFIELD,     Mem: SA IT/9/22,SC22/WG21
        NSW 2131, AUSTRALIA




Author: fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON)
Date: Sat, 22 May 1993 17:21:53 GMT
Raw View
maxtal@physics.su.OZ.AU (John Max Skaller) writes:

>fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON) writes:
>
>>*Perhaps* it would be better to make memberwise assignment for
>>variant members actually be type-changing reinitialization. (Comments, Max?)
>
> There are two reasons not to do this (at least).
>
> The first reason is that it isnt safe.
>
> The second reason is its not obvious how to define it.
>That is: if you define it as *always* doing a reinitialisation,
>then you cannot ever apply a user defined assignment operator.

Sorry, I don't think I made myself clear: I didn't mean make any assignment
for variants be reinitialization, I meant make *memberwise* assignment
for variants be reinitialization. When is memberwise assignment used?
Only to generate a default assignment operator, if there is no user-defined
assignment operator for a type.

In other words, I was suggesting that perhaps you could change the rule for
compiler-generated assignment operators to use reinitialization
for variant members, and member-wise assignment for non-variant members.
The reinitialization would only apply to variants contained in structs
or classes.

But this is not particularly important, since you can always write
your own assignment operator which does what you want.

>>The actual storage for the variant value is dynamically allocated, and
>>assignment is implemented by deleting the old value and allocating a new
>>value.
>
> Be careful! A pointer to a variant is a variant,
>so you have not really escaped the problem

I didn't have a pointer to a variant, I had a pointer to a
Struct_Containing_Variant. So it will work.

>>You could use private inheritance instead of having a member variable 'val',
>>it makes little difference. You could even use public inheritance, or
>>equivalently forward the member function value(), if you want to
>>allow client code to do type switching on Mixed variables.
>
> Careful: variants commute with inheritance I think:
>so that inheriting from a variant is distinctly different from
>having a variant member object :-)

Yes, but I was suggesting inheriting Mixed from Dunion, which is
a fully-fledged class, not a variant.

--
Fergus Henderson                     This .signature virus might be
fjh@munta.cs.mu.OZ.AU                getting old, but you still can't
                                     consistently believe it unless you
Linux: Choice of a GNU Generation    copy it to your own .signature file!




Author: rmartin@uunet.uu.net!rcm (Robert Martin)
Date: Mon, 17 May 1993 04:47:25 GMT
Raw View
u90jl@ecs.ox.ac.uk (Jamie Lokier) writes:

>I have often written a piece of code which needed to do something based
>on the type of an object.  I implement this by adding yet
>another virtual function to the base class definition, and overloading
>the definition in the appropriate derived classes.  This is clean.
>Unfortunately, that virtual function is only ever called from one place
>in my program; namely the piece of code which needed it in the first
>place.  Wouldn't it be better, in that case, to move those virtual
>function definitions away from the class definitions, to the piece of
>code that uses them?  With a hint of optimization, and a bit of syntax,
>this leads to the `switch(object)' mechanism I suggested.  I don't see
>how this violates data/code encapsulation at all.

No, IMHO it wouldn't be better.  The reason is that is places code
dependent upon a particular type in a place which is not associated
with that type.  There may be several such "switches" scattered
throughout the code, each serving a different purpose.  It is not
possible to tell how many there are by looking at the class that they
serve.  When new classes are derived, those switch statements will
have to be updated to include the special processing for the new type.
But because those switch statements are not directly associated with
the classes that they serve, there is no good way to find them all.
You must search through all the code, looking at each 'switch' to see
if it should be updated.

Also, you cannot transport a type from one application to another,
because there is code which serves the type scattered throughout the
application.  It is not all in one place.  It could take a lot of work
to dissect the switch statements and disentagle all the code from one
application and reuse it in another.


--
Robert Martin      | Design Consulting       |Training courses offered:
R.C.M. Consulting  | rcm!rmartin@uunet.UU.NET| Object Oriented Analysis
2080 Cranbrook Rd. | Tel: (708) 918-1004     | Object Oriented Design
Green Oaks IL 60048| Fax: (708) 918-1023     | C++




Author: warwick@cs.uq.oz.au (Warwick Allison)
Date: 17 May 93 05:55:08 GMT
Raw View
u90jl@ecs.ox.ac.uk (Jamie Lokier) writes:

>void function(Base *object)
>{
>    switch (object) { // `switch (*object)' perhaps?
>        case Foo:       // Means Foo or anything derived from it.
>           ...   // In here, object temporarily has type `Foo *'.
>            break;
>        case Bar:       // Special_Kind_of_Bar will also get here.
>            ...
>            break;
>        default:        // You can have a default if you want.
>            ...  // It is equivalent to `case Base'.
>            break;
>    }
>}

>This is a clean construct.

On the contrary, it is a backwards construct.  What you have described is
already available - you're just not using it properly:

void function(Base *object)
{
    if (object->HasTheAttributeOfFooWeAreInterestedIn()) {
           ...
    }

    if (object->HasTheAttributeOfBarWeAreInterestedIn()) {
            ...
    }

    ...
}


That is FAR more flexible than the strict switch() used above.  When new
classes are added to the hierarchy, THE CLASSES THEMSELVES decide whether
they have the AttributeOfFoo... - and function() still works fine with
those classes.

Sure, it means Base::HasTheAttribute...WeAreInterestedIn() must exist,
but that is a far weaker constraint (and indeed, no less powerful) than
the one required for the switch() code above.

--
Warwick
  _-_|\      warwick@cs.uq.oz.au            /
 /     * <-- Computer Science Department,  /
 \_.-._/     University of Queensland,    /         How now, Tao cow?
      v      Brisbane, Australia.        /




Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Mon, 17 May 1993 08:20:58 GMT
Raw View
In article <1993May17.032909.4830@uunet.uu.net!rcm> rmartin@uunet.uu.net!rcm (Robert Martin) writes:
>tmb@arolla.idiap.ch (Thomas M. Breuel) writes:
>
>>Type switches themselves are not "in keeping with the OO paradigm".
>>So what? They are still useful, they can be type safe, they can
>>increase software reuse, and they can reduce the necessity of
>>modifications to existing classes.  But the compiler can only warn you
>>about omitted type cases if it sees the type switch at compile time.
>>Hence, it can be useful to force recompilation in a system that uses
>>type switches.
>
>While I agree that there is a time and place for type switches, I
>would not go so far as to say that they are generally useful.  They
>should be strenuously avoided; and used only at great need.  It will
>pay off in maintenance costs and robustness of design to find a way to
>use virtual functions instead of type switches.
>

 I believe that switches are generally useful, including
type switches. I would venture to suggest that type unification
and switching are as fundamental and useful as polymophism, if
not more so.

 The basic picture I have is that there are two forms
of late binding -- vectoring, also called indirection, which
involves functions pointers (of which virtual functions are
a special highly constrained case), and indexing, also
called switching or selection.

 The concepts and mechanisms involved are quite distinct
in my mind, and whichever is appropriate is the one that should
be used, however inc C++ the bias at the moment should indeed
be towards virtual functions because there is no proper
support for the other form.

 The conceptual differences between inheritance/subclassing
virtual functions and unification/selection/switching are sharp:
subclassing and polymorphism introduces an infinite family
of subtypes of a type, whereas unification constructs a finite set
of discrete, independent types.

 I believe that use of alternation -- alternatives --
is extremely common (if statement anyone?) and the distinction
between state and type is often obsured: in particular,
there is a most pernicious technique that is almost univerally
used and rarely noted as a design fault: it represents
selection but the options are not made explicit.

 Consider:

 struct node {
  node* left, *right;
  char op;
 };

used to represent the parse tree of "A = -B*(c+d)".

OK? No, its really bad. [Hint: see that unary minus operator there?]

I call this technique 'direct unification': grab all the features
you need in what *should* be distinct types, and stuff them
in the one struct. Then just use the components that are relevant.

This leads to multiple tests on conditions, threaded code,
obscure bugs, and numerous other nasty things.

If you've seen this in a book, or *still* dont get why its so
bad, then this is just my point: the technique is so ingrained
its not recognized.

[Hint2: there are at least three distinct types of node required for
the above parse tree and they *should* have been declared as separate
types, IMHO].

Why do we do this? Because there is not enough language support
for type selection.  Its existence is not even recognized sometimes :-)


--
        JOHN (MAX) SKALLER,         INTERNET:maxtal@suphys.physics.su.oz.au
 Maxtal Pty Ltd,      CSERVE:10236.1703
        6 MacKay St ASHFIELD,     Mem: SA IT/9/22,SC22/WG21
        NSW 2131, AUSTRALIA




Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Mon, 17 May 1993 08:45:27 GMT
Raw View
In article <1993May17.044725.5041@uunet.uu.net!rcm> rmartin@uunet.uu.net!rcm (Robert Martin) writes:
>u90jl@ecs.ox.ac.uk (Jamie Lokier) writes:
>
>>I have often written a piece of code which needed to do something based
>>on the type of an object.  I implement this by adding yet
>>another virtual function to the base class definition, and overloading
>>the definition in the appropriate derived classes.  This is clean.

 No, its a total abuse of inheritance. The whole point
of bases is that they can be *closed* to modification, yet
*open* to extension.

 If you have to keep adding virtual functions to support new
types you are not only breaking the open/closed principle,
upon which the power of polymorphism is based, but you are failing
to recognize that subclasses are *not new types* but subtypes.

 If you have a finite set of types, and operations
that depend on the types, then what you really need is variants:
or discriminated unions.

 (If your set of types is arbitrarily extensible, then you
really need Smalltalk :-)

>>Unfortunately, that virtual function is only ever called from one place
>>in my program; namely the piece of code which needed it in the first
>>place.  Wouldn't it be better, in that case, to move those virtual
>>function definitions away from the class definitions, to the piece of
>>code that uses them?

 Even better, get rid of the common base and stop
abusing inheritance.  :-) If all the types have something in common,
place it in the type as a contained object: containment
is the best way to do composition, and composition is the basic
mechanism for constructing new, distinct, types.

>With a hint of optimization, and a bit of syntax,
>>this leads to the `switch(object)' mechanism I suggested.  I don't see
>>how this violates data/code encapsulation at all.
>
>No, IMHO it wouldn't be better.  The reason is that it places code
>dependent upon a particular type in a place which is not associated
>with that type.  There may be several such "switches" scattered
>throughout the code, each serving a different purpose.  It is not
>possible to tell how many there are by looking at the class that they
>serve.

 Exactly so. These are the facts of the matter: the use
of the type depends on the context. It is a matter of public
access to use the type as the public sees fit.

>When new classes are derived, those switch statements will
>have to be updated to include the special processing for the new type.

 No they wont: only the switch statements that switch
on the particular set under consideration will have to
be modified. And that is exactly why this should be done
with *unions* and not base classes: the unions are constructed
as you need them, where you need them, to unify whatever
types are required.

>But because those switch statements are not directly associated with
>the classes that they serve, there is no good way to find them all.
>You must search through all the code, looking at each 'switch' to see
>if it should be updated.

 But this is true in general: to find every use of an object
of some type I must search all the code.

 The uses in the switch statement are *public* accesses
and should not be encapulsated in the class.
>
>Also, you cannot transport a type from one application to another,
>because there is code which serves the type scattered throughout the
>application.  It is not all in one place.  It could take a lot of work
>to dissect the switch statements and disentagle all the code from one
>application and reuse it in another.
>

 The problem is not the switch: the switch is correct.
The problem is the use of inheritance: that permanently binds
a fixed set of derived classes to the base.

 Such a base class .. needing constant additions whenever a
new type is invented .. cannot possibly be reusable.
Incompatible versions would spring up everywhere.

 It is better to scatter the relevant code throught
the program because such code is application specific
and hevaily context sensitive anyhow: it is hard to port,
but that isnt important because such code doesn't *need* to
be ported. It isnt an intrinsic property of the type, but
a particular use of the type.

--
        JOHN (MAX) SKALLER,         INTERNET:maxtal@suphys.physics.su.oz.au
 Maxtal Pty Ltd,      CSERVE:10236.1703
        6 MacKay St ASHFIELD,     Mem: SA IT/9/22,SC22/WG21
        NSW 2131, AUSTRALIA




Author: fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON)
Date: Mon, 17 May 1993 08:53:24 GMT
Raw View
rmartin@uunet.uu.net!rcm (Robert Martin) writes:

>u90jl@ecs.ox.ac.uk (Jamie Lokier) writes:
>
>>Like so many other people, I have found the need for typecasing too.  It
>>is possible to do it efficiently without abandoning the ability to
>>inherit from classes found in such selection statements.
[...]
>>This is a clean construct.
>
>I disagree.  Such structures should be avoided where possible.  (And
>it is almost always possible).  Such structures will be difficult to
>maintain, and will be error prone.  This is because when new classes
>are derived from base, you must hunt for all the switch statements
>that test for derivatives of base, and add the appropriate code to
>them.  This defeats one of the great advantages of OO.

With John Skaller's variants proposal, the compiler itself could
detect when a typeswitch missed one of the cases of a variant.
So it would not need to be error-prone.

--
Fergus Henderson                     This .signature virus might be
fjh@munta.cs.mu.OZ.AU                getting old, but you still can't
                                     consistently believe it unless you
Linux: Choice of a GNU Generation    copy it to your own .signature file!




Author: fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON)
Date: Mon, 17 May 1993 09:40:29 GMT
Raw View
rmartin@uunet.uu.net!rcm (Robert Martin) writes:

>tmb@arolla.idiap.ch (Thomas M. Breuel) writes:
>
>>Type switches themselves are not "in keeping with the OO paradigm".
>>So what? They are still useful, they can be type safe, they can
>>increase software reuse, and they can reduce the necessity of
>>modifications to existing classes.
>
>While I agree that there is a time and place for type switches, I
>would not go so far as to say that they are generally useful.  They
>should be strenuously avoided; and used only at great need.  It will
>pay off in maintenance costs and robustness of design to find a way to
>use virtual functions instead of type switches.

Using N virtual functions instead of a discriminated union containing N
types is not in itself an advantage. Consider adding a new entity (eg.
adding a new token type in the parsing example).  If these entities are
represented as pure virtual functions in the base class, then adding a
new entity requires adding a new pure virtual function to the base
class which in turn requires adding an implementation of that function
to every derived class. How does this reduce maintenance costs or
increase robustness?

C++ should support both OOP and non-OOP programming styles, IMHO.  Each
style has it's place.  Unfortunately C++'s support for discriminated
unions is (as I keep remarking) virtually non-existant.

--
Fergus Henderson                     This .signature virus might be
fjh@munta.cs.mu.OZ.AU                getting old, but you still can't
                                     consistently believe it unless you
Linux: Choice of a GNU Generation    copy it to your own .signature file!




Author: tmb@arolla.idiap.ch (Thomas M. Breuel)
Date: 17 May 93 15:35:40
Raw View
>>>>> On Mon, 17 May 1993 03:29:09 GMT, rmartin@uunet.uu.net!rcm (Robert Martin) said:
> tmb@arolla.idiap.ch (Thomas M. Breuel) writes:
>>Type switches themselves are not "in keeping with the OO paradigm".
>>So what? They are still useful, they can be type safe, they can
>>increase software reuse, and they can reduce the necessity of
>>modifications to existing classes.  But the compiler can only warn you
>>about omitted type cases if it sees the type switch at compile time.
>>Hence, it can be useful to force recompilation in a system that uses
>>type switches.
>
> While I agree that there is a time and place for type switches, I
> would not go so far as to say that they are generally useful.  They
> should be strenuously avoided; and used only at great need.  It will
> pay off in maintenance costs and robustness of design to find a way to
> use virtual functions instead of type switches.

Virtual functions (or, more generally, data driven programming) are
useful if you have a mostly fixed interface but intend to add lots of
new types/implementations.  Type switches are useful if you have a
mostly fixed set of datatypes but intend to add lots of new functions
to them as your program evolves.  Trees and algorithms on trees are a
canonical example of the latter situation.

     Thomas.


















Author: rmartin@uunet.uu.net!rcm (Robert Martin)
Date: Mon, 17 May 1993 15:20:39 GMT
Raw View
maxtal@physics.su.OZ.AU (John Max Skaller) writes:

> I believe that switches are generally useful, including
>type switches. I would venture to suggest that type unification
>and switching are as fundamental and useful as polymophism, if
>not more so.

[ some comments elided ]

> Consider:
> struct node {
>  node* left, *right;
>  char op;
> };

>used to represent the parse tree of "A = -B*(c+d)".

>OK? No, its really bad. [Hint: see that unary minus operator there?]

>I call this technique 'direct unification': grab all the features
>you need in what *should* be distinct types, and stuff them
>in the one struct. Then just use the components that are relevant.

>This leads to multiple tests on conditions, threaded code,
>obscure bugs, and numerous other nasty things.

I quite agree that unifying the nodes of the parse tree into a single
overloaded structure is dangerous.  I would not recommend it.

>[Hint2: there are at least three distinct types of node required for
>the above parse tree and they *should* have been declared as separate
>types, IMHO].

I agree, the different types of nodes should be distinct types.
However, I do not see how that leads inescapably to typecasing...
The nodes of the parse tree should be able to contain all the
functions which are specific to themselves.  i.e. the unary negation
node should be able to negate its subordinate value.

(by the way, I count 4 different types of nodes, negation, addition,
multiplication and assignment.)
--
Robert Martin      | Design Consulting       |Training courses offered:
R.C.M. Consulting  | rcm!rmartin@uunet.UU.NET| Object Oriented Analysis
2080 Cranbrook Rd. | Tel: (708) 918-1004     | Object Oriented Design
Green Oaks IL 60048| Fax: (708) 918-1023     | C++




Author: rmartin@uunet.uu.net!rcm (Robert Martin)
Date: Mon, 17 May 1993 15:32:38 GMT
Raw View
maxtal@physics.su.OZ.AU (John Max Skaller) writes:

>In article <1993May17.044725.5041@uunet.uu.net!rcm> rmartin@uunet.uu.net!rcm (Robert Martin) writes:
>>But because those switch statements are not directly associated with
>>the classes that they serve, there is no good way to find them all.
>>You must search through all the code, looking at each 'switch' to see
>>if it should be updated.

> But this is true in general: to find every use of an object
>of some type I must search all the code.

Ah, but then you violate the open/closed principle, because in order
to extend a class through derivation, you must hunt through reams of
code to find all the switch statements that might be affected.  Thus
the class is not truly open for extension.

This happens in traditional programming all the time.  The
dependencies of one piece of code upon another become so tangled and
complex that it becomes impractical to add more features.  This is the
kind of structure that you build when you proliferate typcase switches
all through the code.

[....]

> Such a base class .. needing constant additions whenever a
>new type is invented .. cannot possibly be reusable.
>Incompatible versions would spring up everywhere.

I agree.  Changing the base class in order to derive a new class
should be avoided just as much as switching on the type.

> It is better to scatter the relevant code throught
>the program because such code is application specific
>and hevaily context sensitive anyhow: it is hard to port,
>but that isnt important because such code doesn't *need* to
>be ported. It isnt an intrinsic property of the type, but
>a particular use of the type.

I agree in part, and I disagree in part.  Application specific usage
of a type should not be written into the type itself.  Either a new
application specific type should be derived, or an application agent
class should be created which uses the type.  In neither case should
such usages be scattered throughout the code.


--
Robert Martin      | Design Consulting       |Training courses offered:
R.C.M. Consulting  | rcm!rmartin@uunet.UU.NET| Object Oriented Analysis
2080 Cranbrook Rd. | Tel: (708) 918-1004     | Object Oriented Design
Green Oaks IL 60048| Fax: (708) 918-1023     | C++




Author: rmartin@uunet.uu.net!rcm (Robert Martin)
Date: Mon, 17 May 1993 15:37:53 GMT
Raw View
fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON) writes:

>rmartin@uunet.uu.net!rcm (Robert Martin) writes:

>>u90jl@ecs.ox.ac.uk (Jamie Lokier) writes:
>>
>>>Like so many other people, I have found the need for typecasing too.  It
>>>is possible to do it efficiently without abandoning the ability to
>>>inherit from classes found in such selection statements.
>[...]
>>>This is a clean construct.
>>
>>I disagree.  Such structures should be avoided where possible.  (And
>>it is almost always possible).  Such structures will be difficult to
>>maintain, and will be error prone.  This is because when new classes
>>are derived from base, you must hunt for all the switch statements
>>that test for derivatives of base, and add the appropriate code to
>>them.  This defeats one of the great advantages of OO.

>With John Skaller's variants proposal, the compiler itself could
>detect when a typeswitch missed one of the cases of a variant.
>So it would not need to be error-prone.

If you expect the compiler to complain, then all the variants will
require recompiling whenver you derive a new class.  I don't think
this is viable.  And it violates open/closed since the class cannot be
extended without affecting other existing code which is closed.

If you expect a runtime error, then I contend that
the process *is* error prone.  Runtime errors were what we would have
had with a regular switch statement.
--
Robert Martin      | Design Consulting       |Training courses offered:
R.C.M. Consulting  | rcm!rmartin@uunet.UU.NET| Object Oriented Analysis
2080 Cranbrook Rd. | Tel: (708) 918-1004     | Object Oriented Design
Green Oaks IL 60048| Fax: (708) 918-1023     | C++




Author: rmartin@uunet.uu.net!rcm (Robert Martin)
Date: Mon, 17 May 1993 15:42:00 GMT
Raw View
fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON) writes:

>Using N virtual functions instead of a discriminated union containing N
>types is not in itself an advantage. Consider adding a new entity (eg.
>adding a new token type in the parsing example).  If these entities are
>represented as pure virtual functions in the base class, then adding a
>new entity requires adding a new pure virtual function to the base
>class which in turn requires adding an implementation of that function
>to every derived class. How does this reduce maintenance costs or
>increase robustness?

IMHO, if the addition of a new subclass forces you to change your base
class, then you probably have a design problem.  I know that this is
easy to say, but I believe it.

If upon creating new derived classes, your design forces you to change
existing code, then the design has a problem IMHO.  Derivation should
be open and existing code should be closed.


--
Robert Martin      | Design Consulting       |Training courses offered:
R.C.M. Consulting  | rcm!rmartin@uunet.UU.NET| Object Oriented Analysis
2080 Cranbrook Rd. | Tel: (708) 918-1004     | Object Oriented Design
Green Oaks IL 60048| Fax: (708) 918-1023     | C++




Author: hartman@ulogic.UUCP (Richard M. Hartman)
Date: 17 May 93 18:44:09 GMT
Raw View
In article <1993May17.084527.21353@ucc.su.OZ.AU> maxtal@physics.su.OZ.AU (John Max Skaller) writes:
>In article <1993May17.044725.5041@uunet.uu.net!rcm> rmartin@uunet.uu.net!rcm (Robert Martin) writes:
>>u90jl@ecs.ox.ac.uk (Jamie Lokier) writes:
>>
>>>I have often written a piece of code which needed to do something based
>>>on the type of an object.  I implement this by adding yet
>>>another virtual function to the base class definition, and overloading
>>>the definition in the appropriate derived classes.  This is clean.
>
> No, its a total abuse of inheritance. The whole point
>of bases is that they can be *closed* to modification, yet
>*open* to extension.

What!??!?  Unless I am misunderstanding something here, what Robert
said is exactly what "virtual" was created for!  Max's statement
seems to cover only non-virtual member functions (closed to
modification).  The whole point of virtual member functions was
the ability to alter the behavior of a derived object to be
appropriate to that object!

That is, if "stop()" on a WheeledThing is implemented by calling
"driver->putFeetOn(ground)", you still might want to implement
"stop()" for "Automobile" by calling "driver->putFeetOn(brake)",
where Automobile is derived from WheeledThing.


> If you have a finite set of types, and operations
>that depend on the types, then what you really need is variants:
>or discriminated unions.
>
> (If your set of types is arbitrarily extensible, then you
>really need Smalltalk :-)

In which every member function is virtual... :-D


  -Richard Hartman
  hartman@ulogic.COM

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Just because you're paranoid doesn't mean they aren't to get you....





Author: duvalld@liberty.uc.wlu.edu (Danek Duvall)
Date: Mon, 17 May 1993 14:32:42 GMT
Raw View
Jeez. This thread is getting confusing really fast. Anyway, I've
come across a problem in my early attempts at C++ that's along this
vein. I ended up using a switch statement, but kicked myself doing
it. I'm sure there's got to be a better way.

My problem involves dynamic creation of objects (sorry if my
terminology is wrong -- please correct it nicely). I'm writing a
modified drawing program (for Windoze, using VC++). The user chooses
an 'object' from the menu -- a circle or a line. Then, depending on
which the user chooses, I need to create an object and stick it in
a list. Once that's done, I can use virtual functions to modify it
in whatever way I want. But how do you instantiate(?) a class at
runtime (using C++). MFC has a way of doing this, provided you
know the name of the class. But you can't pass the name through a
string -- it has to be a literal token.

Have I designed the thing all wrong? Should I (if I could) be using
another language? Can I just use the one switch statement and promise
never to use another? :-)

I'd really appreciate any ideas, suggestions, or advice on this. As
it is, it works, but it could be better.

Thanks,
Danek
--
Danek Duvall: Washington and Lee U.
Internet: duvalld@liberty.uc.wlu.edu
***




Author: fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON)
Date: Mon, 17 May 1993 22:46:12 GMT
Raw View
rmartin@uunet.uu.net!rcm (Robert Martin) writes:

>fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON) writes:
>
>>rmartin@uunet.uu.net!rcm (Robert Martin) writes:
>
>>>u90jl@ecs.ox.ac.uk (Jamie Lokier) writes:
>>>
>>>>Like so many other people, I have found the need for typecasing too.  It
>>>>is possible to do it efficiently without abandoning the ability to
>>>>inherit from classes found in such selection statements.
>>[...]
>>>>This is a clean construct.
>>>
>>>I disagree.  Such structures should be avoided where possible.  (And
>>>it is almost always possible).  Such structures will be difficult to
>>>maintain, and will be error prone.  This is because when new classes
>>>are derived from base, you must hunt for all the switch statements
>>>that test for derivatives of base, and add the appropriate code to
>>>them.  This defeats one of the great advantages of OO.
>
>>With John Skaller's variants proposal, the compiler itself could
>>detect when a typeswitch missed one of the cases of a variant.
>>So it would not need to be error-prone.
>
>If you expect the compiler to complain, then all the variants will
>require recompiling whenver you derive a new class.  I don't think
>this is viable.  And it violates open/closed since the class cannot be
>extended without affecting other existing code which is closed.

I was not very clear in explaining what I meant above.
WIth John Skaller's proposal (unlike Lokier's construct) the
set of types that you are switching on is explicit in the
variant definition. So recompilation need only occur when
you explicitly change that variant definition. You can derive
new classes from the variants component types, but you
won't be able to switch on those subtypes unless you modify the
variant definition. Thus the variant definition clearly documents
which types are being switch on.

Mind you, adding a new type to a variant's type set is not the
only way of extending it. You can inherit from the variant.
You can inherit from one of the types that makes up the variants type set.
You can encapsulate the variant into a class and inherit from the class.
You can provide new non-member functions acting on the variant.
... the possibilites are almost endless :-)

>If you expect a runtime error, [...]

No, I agree that runtime error detection would be distinctly second-rate.
That is what we would get if we end up using RTTI for all our heterogenous
collections, though ;-)

--
Fergus Henderson                     This .signature virus might be
fjh@munta.cs.mu.OZ.AU                getting old, but you still can't
                                     consistently believe it unless you
Linux: Choice of a GNU Generation    copy it to your own .signature file!




Author: fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON)
Date: Mon, 17 May 1993 22:55:56 GMT
Raw View
rmartin@uunet.uu.net!rcm (Robert Martin) writes:

>maxtal@physics.su.OZ.AU (John Max Skaller) writes:

>> I believe that switches are generally useful, including
>>type switches. I would venture to suggest that type unification
>>and switching are as fundamental and useful as polymophism, if
>>not more so.

>> Consider:
>> struct node {
>>  node* left, *right;
>>  char op;
>> };

>>OK? No, its really bad.

>I quite agree that unifying the nodes of the parse tree into a single
>overloaded structure is dangerous.  I would not recommend it.
>
>I agree, the different types of nodes should be distinct types.
>However, I do not see how that leads inescapably to typecasing...
>The nodes of the parse tree should be able to contain all the
>functions which are specific to themselves.  i.e. the unary negation
>node should be able to negate its subordinate value.

The problem is that there are too many possible operations on trees and
the only fundamental operation that could be used to implement all the
others is type switching.  So unless you use some form of type
switching, you are left with the two alternatives of putting all the
possible operations in the class as virtual member functions
(impossible, there are too many of them :-), or putting only a
particular subset of the possible operations. Of course you choose the
latter approach, but then inevitably there will come a time when you
will decide that the subset you originally chose wasn't exactly right,
and you need to add a new virtual function to the base class. The base
class can thus never be considered closed.

--
Fergus Henderson                     This .signature virus might be
fjh@munta.cs.mu.OZ.AU                getting old, but you still can't
                                     consistently believe it unless you
Linux: Choice of a GNU Generation    copy it to your own .signature file!




Author: tmb@arolla.idiap.ch (Thomas M. Breuel)
Date: 18 May 93 02:15:02
Raw View
>>>>> On Mon, 17 May 1993 15:37:53 GMT, rmartin@uunet.uu.net!rcm (Robert Martin) said:
> If you expect the compiler to complain, then all the variants will
> require recompiling whenver you derive a new class.  I don't think
> this is viable.

Recompilations in a tagged union scheme are only necessary if you add
a new tag to the tagged union itself, not if you derive from one of
its member types.

You can try it out for yourself.  SML defines and implements this
feature.  In my experience, it works quite well.

     Thomas.




Author: ole@delab.sintef.no (Ole-Hjalmar Kristensen)
Date: 18 May 93 12:01:46
Raw View
In article <1993May17.152039.6557@uunet.uu.net!rcm> rmartin@uunet.uu.net!rcm (Robert Martin) writes:

   I quite agree that unifying the nodes of the parse tree into a single
   overloaded structure is dangerous.  I would not recommend it.

   >[Hint2: there are at least three distinct types of node required for
   >the above parse tree and they *should* have been declared as separate
   >types, IMHO].

   I agree, the different types of nodes should be distinct types.
   However, I do not see how that leads inescapably to typecasing...
   The nodes of the parse tree should be able to contain all the
   functions which are specific to themselves.  i.e. the unary negation
   node should be able to negate its subordinate value.

   (by the way, I count 4 different types of nodes, negation, addition,
   multiplication and assignment.)
   --
I don't think these are the different types. They are the prefix,
infix and postfix operators which should be containd in separate
classes, because they are handled differently. The actual operator can
be encoded in the op field. (or you can use a function pointer/virtual
function)

--
Ole-Hj. Kristensen

"If you are going to shoot, shoot, don't talk."
      -- Il Bruto




Author: rmartin@rcmcon.com (Robert Martin)
Date: Tue, 18 May 1993 14:32:14 GMT
Raw View
duvalld@liberty.uc.wlu.edu (Danek Duvall) writes:

>Jeez. This thread is getting confusing really fast. Anyway, I've
>come across a problem in my early attempts at C++ that's along this
>vein. I ended up using a switch statement, but kicked myself doing
>it. I'm sure there's got to be a better way.

[...Danek wonders how to initially create objects without a switch
statement ...]

The ultimate creator of an object must know what kind of object he is
creating.  Often, deciding which kind of object to create is the
result of a switch or long if/else process.  There is nothing wrong
with this.  The decision has to be made somewhere.  As long as the
decision is made in just that one place, everything is fine.

The problem with typecasing (as I see it) is that it is a cancer which
spreads through your code.  As the program evolves more and more
switch statements are needed to handle the new features.  When more
types are added, all the switch statements need to be updated to
handle the new type.

If all the code pertaining to a certain type is kept within that type,
and is accessed via virtual functions, then the 'type' decision has to
be made only once, at creation.  No other agent within the code will
need to make that decision again.
--
Robert Martin       | Design Consulting   | Training courses offered:
R.C.M. Consulting   | rmartin@rcmcon.com  |   Object Oriented Analysis
2080 Cranbrook Rd.  | Tel: (708) 918-1004 |   Object Oriented Design
Green Oaks IL 60048 | Fax: (708) 918-1023 |   C++




Author: rmartin@rcmcon.com (Robert Martin)
Date: Tue, 18 May 1993 14:55:03 GMT
Raw View
fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON) writes:

>>>With John Skaller's variants proposal, the compiler itself could
>>>detect when a typeswitch missed one of the cases of a variant.
>>>So it would not need to be error-prone.

>I was not very clear in explaining what I meant above.
>WIth John Skaller's proposal (unlike Lokier's construct) the
>set of types that you are switching on is explicit in the
>variant definition. So recompilation need only occur when
>you explicitly change that variant definition.

Yes, and I think that leads to a serious problem...

Let's say we have the following set of variants.

 Variant V[A,X];
 Variant V2[B,C,X];
 Variant V3[X,Q,A];

These variant definitions are scattered throughout the code.  Some of
them are local variables, hidden down in inner loops.

Now we discover that there is another kind of object, similar to an X,
that can be used wherever an X can be used.  Call this new object Z.
We would like to derive Z from X, but because X is used in Variants,
we wonder if Z should not take its place within the variants.  If we
choose the latter, we must find all the variants which include X and
add Z.  We must also publish a rule that in any variant which has an
X, a Z must be present as well.

Finding all the variant declarations is non-trivial.  There is no
compiler mechanisms which will guarantee that we have found the all.
The failure to find one will result in a (perhaps silent) runtime
error.

Users of X will be angry once Z objects come to the fore.  They will
have to go back and change all their variant code.  This violates
open/closed.

Is there a solution to this problem?
--
Robert Martin       | Design Consulting   | Training courses offered:
R.C.M. Consulting   | rmartin@rcmcon.com  |   Object Oriented Analysis
2080 Cranbrook Rd.  | Tel: (708) 918-1004 |   Object Oriented Design
Green Oaks IL 60048 | Fax: (708) 918-1023 |   C++




Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Tue, 18 May 1993 16:29:58 GMT
Raw View
In article <1993May17.152039.6557@uunet.uu.net!rcm> rmartin@uunet.uu.net!rcm (Robert Martin) writes:
>maxtal@physics.su.OZ.AU (John Max Skaller) writes:
>
>> I believe that switches are generally useful, including
>>type switches. I would venture to suggest that type unification
>>and switching are as fundamental and useful as polymophism, if
>>not more so.
>
>[ some comments elided ]
>
>> Consider:
>> struct node {
>>  node* left, *right;
>>  char op;
>> };
>
>>used to represent the parse tree of "A = -B*(c+d)".
>
>>OK? No, its really bad. [Hint: see that unary minus operator there?]
>
>>I call this technique 'direct unification': grab all the features
>>you need in what *should* be distinct types, and stuff them
>>in the one struct. Then just use the components that are relevant.
>
>>This leads to multiple tests on conditions, threaded code,
>>obscure bugs, and numerous other nasty things.
>
>I quite agree that unifying the nodes of the parse tree into a single
>overloaded structure is dangerous.  I would not recommend it.
>
>>[Hint2: there are at least three distinct types of node required for
>>the above parse tree and they *should* have been declared as separate
>>types, IMHO].
>
>I agree, the different types of nodes should be distinct types.
>However, I do not see how that leads inescapably to typecasing...
>The nodes of the parse tree should be able to contain all the
>functions which are specific to themselves.  i.e. the unary negation
>node should be able to negate its subordinate value.

 But it had better not do that.. because I dont *want* to
evaluate the tree, I want to print it in reverse polish,
and forward polish, and reprint it in infix and generate machine code
and generate Pascal code and ....

 None of these operations are *intrinsic* to the
data structure, so I dont *want* to encapsulate them in the class,
because I dont know what I'm going to use the parse tree *for*.

 If I had a single specific purpose (behaviour) I could
indeed do what you suggest. But I dont. So an Object Oriented
approach would actually limit what I can do.

 You are right it doesnt lead *inevitably* to typecasing.
But if you want late binding, then in this case OO is useless.
Its the wrong sort of late binding. What I want is variants:
they *also* implement late binding, but of a different kind.
The kind I want. (In this case :-)

>
>(by the way, I count 4 different types of nodes, negation, addition,
>multiplication and assignment.)

 Depends how you count them, I did not go that far,
and defined only 'binop', 'unop', and 'id' (you better have
an 'identifier' node: by your counting that makes 5 :-)

 Without variants, this example is really
hard to code. I've *tried* in a real parser, to use
the idiom for discriminated unions. Yuk. I'll use RTTI
next time if the committee doesnt go for variants.
(Or maybe try for SML .. or Smalltalk)

 :-)

--
        JOHN (MAX) SKALLER,         INTERNET:maxtal@suphys.physics.su.oz.au
 Maxtal Pty Ltd,      CSERVE:10236.1703
        6 MacKay St ASHFIELD,     Mem: SA IT/9/22,SC22/WG21
        NSW 2131, AUSTRALIA




Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Tue, 18 May 1993 17:35:32 GMT
Raw View
In article <1993May17.153238.6649@uunet.uu.net!rcm> rmartin@uunet.uu.net!rcm (Robert Martin) writes:
>maxtal@physics.su.OZ.AU (John Max Skaller) writes:
>
>>In article <1993May17.044725.5041@uunet.uu.net!rcm> rmartin@uunet.uu.net!rcm (Robert Martin) writes:
>>>But because those switch statements are not directly associated with
>>>the classes that they serve, there is no good way to find them all.
>>>You must search through all the code, looking at each 'switch' to see
>>>if it should be updated.
>
>> But this is true in general: to find every use of an object
>>of some type I must search all the code.
>
>Ah, but then you violate the open/closed principle, because in order
>to extend a class through derivation, you must hunt through reams of
>code to find all the switch statements that might be affected.  Thus
>the class is not truly open for extension.

 I agreed with your original point: so I tried to
follow the logic from the other point of view: that the switches
were OK.. and concluded that the common base was the
root of the design problem instead :-)
>
>This happens in traditional programming all the time.  The
>dependencies of one piece of code upon another become so tangled and
>complex that it becomes impractical to add more features.  This is the
>kind of structure that you build when you proliferate typcase switches
>all through the code.

 True.

>> Such a base class .. needing constant additions whenever a
>>new type is invented .. cannot possibly be reusable.
>>Incompatible versions would spring up everywhere.
>
>I agree.  Changing the base class in order to derive a new class
>should be avoided just as much as switching on the type.

 Switching on distinct types *not* assumed
to have a common base is not so bad if it can be localised
with a discriminated union.

 The switches dont *add* any functionality to
any of the classes, they are just ordinary uses of the class,
so they dont constrain extensions to the classes, or any
other uses.
>
>> It is better to scatter the relevant code throught
>>the program because such code is application specific
>>and hevaily context sensitive anyhow: it is hard to port,
>>but that isnt important because such code doesn't *need* to
>>be ported. It isnt an intrinsic property of the type, but
>>a particular use of the type.
>
>I agree in part, and I disagree in part.  Application specific usage
>of a type should not be written into the type itself.  Either a new
>application specific type should be derived, or an application agent
>class should be created which uses the type.  In neither case should
>such usages be scattered throughout the code.
>

 That is a good point, and indeed, in Eiffel, you would
naturally devise a new type .. because thats all you can do.
(no global functions .. only classes)

--
        JOHN (MAX) SKALLER,         INTERNET:maxtal@suphys.physics.su.oz.au
 Maxtal Pty Ltd,      CSERVE:10236.1703
        6 MacKay St ASHFIELD,     Mem: SA IT/9/22,SC22/WG21
        NSW 2131, AUSTRALIA




Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Tue, 18 May 1993 17:45:02 GMT
Raw View
In article <1993May17.153753.6738@uunet.uu.net!rcm> rmartin@uunet.uu.net!rcm (Robert Martin) writes:
>fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON) writes:
>
>>rmartin@uunet.uu.net!rcm (Robert Martin) writes:
>
>>With John Skaller's variants proposal, the compiler itself could
>>detect when a typeswitch missed one of the cases of a variant.
>>So it would not need to be error-prone.
>
>If you expect the compiler to complain, then all the variants will
>require recompiling whenver you derive a new class.

 Wait. Deriving new classes is irrelevant. A variant
is a set of types. Adding a new type to a variant requires recompilation.
Deriving new classes is unrelated.


>this is viable.  And it violates open/closed since the class cannot be
>extended without affecting other existing code which is closed.

 Well, thats not so: it comletely preserves the open
closed principle for classes.  However, variants themselves
are not classes. They are not open to have new types added
when they are closed (as classes are).

>
>If you expect a runtime error, then I contend that
>the process *is* error prone.  Runtime errors were what we would have
>had with a regular switch statement.

 Variants are completely safe both statically and dynamically.
No run time error is possible.

 variant Tset [T1, T2, T3];

 f(Tset t) {
  select(t) {
  type(T1 t1) {  ... t1 ...}
  ..
  }
 }

No possibility of error. You cant reference the wrong component
because the compiler 'selects' only code generated for
the component that is correct.

--
        JOHN (MAX) SKALLER,         INTERNET:maxtal@suphys.physics.su.oz.au
 Maxtal Pty Ltd,      CSERVE:10236.1703
        6 MacKay St ASHFIELD,     Mem: SA IT/9/22,SC22/WG21
        NSW 2131, AUSTRALIA




Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Tue, 18 May 1993 17:58:37 GMT
Raw View
In article <1993May17.154200.6827@uunet.uu.net!rcm> rmartin@uunet.uu.net!rcm (Robert Martin) writes:

>IMHO, if the addition of a new subclass forces you to change your base
>class, then you probably have a design problem.  I know that this is
>easy to say, but I believe it.
>
>If upon creating new derived classes, your design forces you to change
>existing code, then the design has a problem IMHO.  Derivation should
>be open and existing code should be closed.
>

 I agree. In particular, a subclass is a subtype and NOT
a new distinct type.

 However, the point of variants or dunions (discriminated
unions) is that they are not classes, indeed, variants
are not even types.

 Variants provide late binding, but a completely
different sort of late binding to virtual functions, and they
have almost the opposite properties to classes in very many respects.

 Its not surprising that would have different open/closed properties.

 Its even less surpising (to me) that by using *both* classes
and variants, you can 'optimise' your code to obtain exactly the
right mix of 'encapsulation' versus 'distribution'.

--
        JOHN (MAX) SKALLER,         INTERNET:maxtal@suphys.physics.su.oz.au
 Maxtal Pty Ltd,      CSERVE:10236.1703
        6 MacKay St ASHFIELD,     Mem: SA IT/9/22,SC22/WG21
        NSW 2131, AUSTRALIA




Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Tue, 18 May 1993 19:14:40 GMT
Raw View
In article <1993May18.145503.1855@rcmcon.com> rmartin@rcmcon.com (Robert Martin) writes:
>
>Let's say we have the following set of variants.
>
> Variant V[A,X];
> Variant V2[B,C,X];
> Variant V3[X,Q,A];
>
>These variant definitions are scattered throughout the code.  Some of
>them are local variables, hidden down in inner loops.

 Why not put them in a header file?

>
>Now we discover that there is another kind of object, similar to an X,
>that can be used wherever an X can be used.  Call this new object Z.
>We would like to derive Z from X, but because X is used in Variants,
>we wonder if Z should not take its place within the variants.  If we
>choose the latter, we must find all the variants which include X and
>add Z.  We must also publish a rule that in any variant which has an
>X, a Z must be present as well.

 It obviously wasnt a subtype of X then.

 If it was, it would be correctly processed *as a* Z.

 If not, all the *polymorphism* in your programs would fail too.

 Variants are for discrete, distinct types.

 Subclassing is for subtypes.. not new types.

 If you want to have a new type that is a subtype you
already have a design error :-)

--
        JOHN (MAX) SKALLER,         INTERNET:maxtal@suphys.physics.su.oz.au
 Maxtal Pty Ltd,      CSERVE:10236.1703
        6 MacKay St ASHFIELD,     Mem: SA IT/9/22,SC22/WG21
        NSW 2131, AUSTRALIA




Author: fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON)
Date: Wed, 19 May 1993 06:06:03 GMT
Raw View
rmartin@rcmcon.com (Robert Martin) writes:

>The problem with typecasing (as I see it) is that it is a cancer which
>spreads through your code.  As the program evolves more and more
>switch statements are needed to handle the new features.  When more
>types are added, all the switch statements need to be updated to
>handle the new type.

To continue the classes vs. variants analogy (well, parody :-):

The problem with inheritance (as I see it) is that it is a cancer which
spreads through your code.  As the program evolves, more and more
virtual functions are needed to handle the new features. When more
virtual functions are added, all the derived classes need to be updated
to handle the new virtual function.

--
Fergus Henderson                     This .signature virus might be
fjh@munta.cs.mu.OZ.AU                getting old, but you still can't
                                     consistently believe it unless you
Linux: Choice of a GNU Generation    copy it to your own .signature file!




Author: fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON)
Date: Wed, 19 May 1993 06:30:47 GMT
Raw View
rmartin@rcmcon.com (Robert Martin) writes:

>fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON) writes:
>
>>>>With John Skaller's variants proposal, the compiler itself could
>>>>detect when a typeswitch missed one of the cases of a variant.
>>>>So it would not need to be error-prone.
>
>>I was not very clear in explaining what I meant above.
>>WIth John Skaller's proposal (unlike Lokier's construct) the
>>set of types that you are switching on is explicit in the
>>variant definition. So recompilation need only occur when
>>you explicitly change that variant definition.
>
>Yes, and I think that leads to a serious problem...
>
>Let's say we have the following set of variants.
>
> Variant V[A,X];
> Variant V2[B,C,X];
> Variant V3[X,Q,A];
>
>These variant definitions are scattered throughout the code.  Some of
>them are local variables, hidden down in inner loops.
>
>Now we discover that there is another kind of object, similar to an X,
>that can be used wherever an X can be used.  Call this new object Z.
>We would like to derive Z from X, but because X is used in Variants,
>we wonder if Z should not take its place within the variants.

Well, if Z really can be used whenever an X can be used, then
you should use derived X from Z and use virtual functions
rather than type switches to discriminate between X and Z.
Just because X is used in variants is no reason to abandon the
use of inheritance if inheritance works well.

>If we choose the latter, we must find all the variants which include X and
>add Z.  We must also publish a rule that in any variant which has an
>X, a Z must be present as well.
>
>Finding all the variant declarations is non-trivial.  There is no
>compiler mechanisms which will guarantee that we have found the all.
>The failure to find one will result in a (perhaps silent) runtime
>error.

If it really is the case that *any* variant which has an X *must* also
have a Z then you would be much better off deriving Z from X. But
assuming that you decided instead to add the Z to the variants, then
your code would not get a runtime error. Instead, there are two
possibilities:

    (1) The variant is used in the interface to some function.
    In this case, you would get a compile error if you try to
 pass a Z (or a variant including a Z) to that function.

    (2) The variant is used only in the implementation of some function.
 In this case, there is no way that any Z objects can ever be
 part of this variant. The function will continue to work just as
 it did before. There would be no reason to add Z to this variant.

--
Fergus Henderson                     This .signature virus might be
fjh@munta.cs.mu.OZ.AU                getting old, but you still can't
                                     consistently believe it unless you
Linux: Choice of a GNU Generation    copy it to your own .signature file!




Author: tmb@arolla.idiap.ch (Thomas M. Breuel)
Date: 19 May 1993 13:05:28 GMT
Raw View
In article <1993May18.145503.1855@rcmcon.com>, you write:
|> >I was not very clear in explaining what I meant above.
|> >WIth John Skaller's proposal (unlike Lokier's construct) the
|> >set of types that you are switching on is explicit in the
|> >variant definition. So recompilation need only occur when
|> >you explicitly change that variant definition.
|>
|> Yes, and I think that leads to a serious problem...
|>
|> Let's say we have the following set of variants.
|>
|>  Variant V[A,X];
|>  Variant V2[B,C,X];
|>  Variant V3[X,Q,A];
|>
|> These variant definitions are scattered throughout the code.  Some of
|> them are local variables, hidden down in inner loops.
|>
|> Now we discover that there is another kind of object, similar to an X,
|> that can be used wherever an X can be used.  Call this new object Z.
|> We would like to derive Z from X, but because X is used in Variants,
|> we wonder if Z should not take its place within the variants.

I don't know what exactly Skaller is proposing, but it seems to
me that there is no problem.  A Z is derived from an X, so it
will simply be treated as an X, unless it is mentioned explicitly
as a separate type in the variant.

If, at some point in your code, you want to treat a Z different from an
X, you have to extend the variant definition to include both Z and X, and
the compiler will tell you which switch statements need updating.

|> Finding all the variant declarations is non-trivial.  There is no
|> compiler mechanisms which will guarantee that we have found the all.
|> The failure to find one will result in a (perhaps silent) runtime
|> error.

Any implementation of variants would only be useful if it
detected such errors at compile time.  I don't see any problem
with doing that.  If a variant can't hold an object of a particular
type, the compiler will detect this when try you assign such
an object to the variant.



Author: rmartin@rcmcon.com (Robert Martin)
Date: Wed, 19 May 1993 14:31:59 GMT
Raw View
maxtal@physics.su.OZ.AU (John Max Skaller) writes:
>>> Consider:
>>> struct node {
>>>  node* left, *right;
>>>  char op;
>>> };
>>>used to represent the parse tree of "A = -B*(c+d)".
>>>[Hint2: there are at least three distinct types of node required for
>>>the above parse tree and they *should* have been declared as separate
>>>types, IMHO].

In article <1993May17.152039.6557@uunet.uu.net!rcm>
rmartin@uunet.uu.net!rcm (Robert Martin) writes:
>>I agree, the different types of nodes should be distinct types.
>>However, I do not see how that leads inescapably to typecasing...
>>The nodes of the parse tree should be able to contain all the
>>functions which are specific to themselves.  i.e. the unary negation
>>node should be able to negate its subordinate value.

maxtal@physics.su.OZ.AU (John Max Skaller) writes:
> But it had better not do that.. because I dont *want* to
>evaluate the tree, I want to print it in reverse polish,
>and forward polish, and reprint it in infix and generate machine code
>and generate Pascal code and ....

> None of these operations are *intrinsic* to the
>data structure, so I dont *want* to encapsulate them in the class,
>because I dont know what I'm going to use the parse tree *for*.

I see your point.   You don't want the nodes of the parse tree to
"know" what purpose they will be used for.  So let me suggest another
route.  Separate the data structure from the intent of the data.

class Node
{
  public:
    virtual void Invoke(NodeProcessor&) const = 0;
};

class BinaryOpNode : public Node
{
  public:
    virtual void Invoke(NodeProcessor& p) const
    {p.BinOp(*this);}
};

class UnaryOpNode : public Node
{
  public:
    virtual void Invoke(NodeProcessor& p) const
    {p.UnaryOp(*this);}
};

class NodeProcessor
{
  public:
    virtual void BinOp(const BinaryOpNode&) = 0;
    virtual void UnaryOp(const UnaryOpNode&) = 0;
};

class PascalGeneratorNodeProcessor : public NodeProcessor {...};
class M68KGeneratorNodeProcessor   : public NodeProcessor {...};
class PostFixPrinterNodeProcessor  : public NodeProcessor {...};

You can derive whatever kind of node processor you like.  You can walk
the tree in whatever order you desire, and then pass the node
processor to the Invoke member of each node.

Since the structure of the tree is not known outside the Nodes, a
NodeProcessor must decide how to walk the nodes.  This can be done by
the NodeProcessor responsible for the processing, or it could be done
by a secondary node processor....

class InOrderNodeProcessor : public NodeProcessor
{
  public:
    InOrderNodeProcessor(NodeProcessor& proc)
    : itsProcessor(proc)
    {}

    virtual void BinaryOp(const BinaryOpNode& n)
    {
        n.left.Invoke(*this);
        n.Invoke(itsProcessor);
        n.right.Invoke(*this);
    }

    virtual void UnaryOp(const UnaryOpNode& n)
    {
        n.operand.Invoke(*this);
        n.Invoke(itsProcessor);
    }

  private:
    NodeProcessor& itsProcessor;
};

This processor walks the tree in order (infix) and invokes another
processor upon each node.

Of course the downside is that the NodeProcessor hierarchy will have
to undergo lots of changes whenever you add a different kind of Node.
But this is no worse than changing all the Selects when you add a
different kind of variant.
--
Robert Martin       | Design Consulting   | Training courses offered:
R.C.M. Consulting   | rmartin@rcmcon.com  |   Object Oriented Analysis
2080 Cranbrook Rd.  | Tel: (708) 918-1004 |   Object Oriented Design
Green Oaks IL 60048 | Fax: (708) 918-1023 |   C++




Author: kanze@us-es.sel.de (James Kanze)
Date: 17 May 93 18:24:03
Raw View
In article <u90jl.180459.14May1993@ecs.ox.ac.uk> u90jl@ecs.ox.ac.uk
(Jamie Lokier) writes:

|> > >Like so many other people, I have found the need for typecasing too.  It
|> > >is possible to do it efficiently without abandoning the ability to
|> > >inherit from classes found in such selection statements.
|> > >
|> > >Here is my suggested syntax for code selection based on class types:
|> > >
|> > >class Base { ... };
|> > >class Foo : Base { ... };
|> > >class Bar : Base { ... };
|> > >class Special_Kind_of_Bar : Bar { ... };
|> > >
|> > >...
|> > >
|> > >void function(Base *object)
|> > >{
|> > >    switch (object) { // `switch (*object)' perhaps?
|> > >
|> > >        case Foo:       // Means Foo or anything derived from it.
|> > >            ...  // In here, object temporarily has type `Foo *'.
|> > >            break;
|> > >
|> > >        case Bar:       // Special_Kind_of_Bar will also get here.
|> > >            ...
|> > >            break;
|> > >
|> > >        default:        // You can have a default if you want.
|> > >            ...  // It is equivalent to `case Base'.
|> > >            break;
|> > >    }
|> > >}
|> >

|> > Your syntax is basically the same as John Skaller's, except that
|> > his is deliberately different from the normal C switch statement
|> > so that (1) it's obvious to the reader that this is a type switch
|> > not a normal switch and (2) to avoid the problem that a switch case
|> > doesn't introduce a new scope.

|> No, the semantics are not the same.  In my construct, `case XXX' means
|> "jump here if the object is of type XXX *or anything derived from it*".
|> So the `case' actually refers to a potentially infinite set of types,
|> which the compiler does not know about when it compiles the module.
|> This retains an important level of abstraction between a particular
|> switch statement and the class heirarchy it is working with.

|> Unlike the conevntional `switch' statement, my `cases' are not testing
|> *equality* of types.  It's more like testing a partial order on them.
|> Perhaps `switch (typeof(object))' would be more appropriate?

This sounds a bit like what is going on in exception handling,
something like the following:

 void
 function( Base* object )
 {
  try
  {
   throw *object ;
  }
  catch( Foo )
  {
   // ...
  }
  catch( Bar )
  {
   // ...
  }
  catch( ... )
  {
   // ...
  }
 }

Of course, this isn't as pretty.  More important, since exceptions are
meant for exceptional cases, the compiler writer will supposedly not
make them very efficient.
--
James Kanze                             email: kanze@us-es.sel.de
GABI Software, Sarl., 8 rue du Faisan, F-67000 Strasbourg, France
Conseils en informatique industrielle --
                   -- Beratung in industrieller Datenverarbeitung




Author: vinoski@apollo.hp.com (Steve Vinoski)
Date: Wed, 19 May 1993 20:33:44 GMT
Raw View
In article <KANZE.93May17182403@slsvdnt.us-es.sel.de> kanze@us-es.sel.de (James Kanze) writes:
> void
> function( Base* object )
> {
>  try
>  {
>   throw *object ;
>  }
>  catch( Foo )
>  {
>   // ...
>  }
>  catch( Bar )
>  {
>   // ...
>  }
>  catch( ... )
>  {
>   // ...
>  }
> }
>
>Of course, this isn't as pretty.  More important, since exceptions are
>meant for exceptional cases, the compiler writer will supposedly not
>make them very efficient.

I don't believe this will work as intended.

Section 15.2, page 356 of the ARM states that "a throw-expression
initializes a temporary object of the static type of the operand of
throw and uses that temporary to initialize the appropriately-typed
variable named in the handler."  In your example, *object is
statically typed as a Base, so the catch(...) clause will *always* be
entered, even if object actually points to a Foo or Bar (which are
both derived from Base in the original example).

For what it's worth, the implementation of exceptions in the HP C++
compiler backs me up.

--steve

Steve Vinoski  (508)436-5904   vinoski@apollo.hp.com
Distributed Object Computing Program
Hewlett-Packard, Chelmsford, MA 01824       These are my opinions.




Author: chased@rbbb.Eng.Sun.COM (David Chase)
Date: 19 May 1993 20:46:50 GMT
Raw View
In article <KANZE.93May17182403@slsvdnt.us-es.sel.de> kanze@us-es.sel.de (James Kanze) writes:
>This [typecasing] sounds a bit like what is going on in exception
>handling, something like the following:

> void
> function( Base* object )
> {
>  try
>  {
>   throw *object ;
>  }
>  catch( Foo )
>  {
>   // ...
>  }
>  catch( Bar )
>  {
>   // ...
>  }
>  catch( ... )
>  {
>   // ...
>  }
> }

>Of course, this isn't as pretty.  More important, since exceptions are
>meant for exceptional cases, the compiler writer will supposedly not
>make them very efficient.

Generally the same idea, but wrong on certain of the specifics:

1. the type thrown for the exception is (as I understand it) the
declared type at the throw site.  You could, of course, use a class
with a virtual member function, say "throw_up" and that would be one
way to get the type casing done for you.

2. The expensive part of exception handling need not be the
type-casting.  Depending upon whether you maintain a linked list of
active handlers, or use a PC-hash table, or use a PC-binary-search
table, it can take somewhere between 5 and 300 instructions per
activation record traversed.

3. The expense of the (sub)type-check depends upon the structure of
the type system.  It need not be expensive -- you can either imagine a
type-by-type lookup matrix, or you can do various ancestor-finding
tricks in a tree.  Note that you may well have multiple versions of a
"type" in your data structure -- the straightforward algorithms are
not always the fastest ones.  The type-by-type table techniques lead
to type-switch being linear in number of cases, with a small constant
factor, whereas the tree-ancestor algorithms tend to be cost log # of
cases, with a somewhat larger constant factor.

The exact choice of algorithm also depends on how you design your type
system.  I understand how to do this quickly for  multiple-implementa-
tion-inheritance, but not for interface inheritance (not yet, anyway).

David Chase
Sun





Author: hartman@ulogic.UUCP (Richard M. Hartman)
Date: 19 May 93 19:46:15 GMT
Raw View
In article <9313916.1629@mulga.cs.mu.OZ.AU> fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON) writes:
>rmartin@rcmcon.com (Robert Martin) writes:
>
>>The problem with typecasing (as I see it) is that it is a cancer which
>>spreads through your code.  As the program evolves more and more
>>switch statements are needed to handle the new features.  When more
>>types are added, all the switch statements need to be updated to
>>handle the new type.
>
>To continue the classes vs. variants analogy (well, parody :-):
>
>The problem with inheritance (as I see it) is that it is a cancer which
>spreads through your code.  As the program evolves, more and more
>virtual functions are needed to handle the new features. When more
>virtual functions are added, all the derived classes need to be updated
>to handle the new virtual function.

Sorry Fergus, but it doesn't hold.  Virtual functions, as long as
they are not "pure" virtuals, can be inheirted.  So if the base class
implements a noop virtual function, all decendants that don't need
to handle that operation don't, with no maintenance need on their
sources.  Only the derived classes that will make use of the new virtual
function need to be updated, not all of them.  I'm with Robert on this one.
There is no getting around the requirement to maintain the multiple
switch statements if you use that method.

  -Richard Hartman
  hartman@uLogic.COM

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
"Ideas are not responsible for the people who believe them."





Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Thu, 20 May 1993 14:56:23 GMT
Raw View
In article <1tdbaoINNmj9@life.ai.mit.edu> tmb@arolla.idiap.ch (Thomas M. Breuel) writes:
>
>I don't know what exactly Skaller is proposing,

 You will soon :-)

>From what I can glean from the above syntax for variants, I don't
>particularly like it, since it doesn't allow you to include the
>same type several times in the variant, something that occurs
>frequently in practice.  I think variant tags should be distinct
>from object types.  All the avove considerations about compile
>time diagnostics and inheritance still apply, though, even if you
>make tags explicit.
>

 Not allowing the same type to appear is quite deliberate.
That is because the semantics of variants are determined
almost exclusively by existing language rules for function
overload resolution, that is, argument matching.

 In effect, you can think of a variant as an overloaded
data object :-)

 A variant is not a type. It is not a discriminated
union. There is no tag you can refer to in the language
definition: there may or may not be in a given implementation
either.

 Variants are not types, and do not have type names.
They can have *typedef* names, but that is purely a macro
shorthand for the expanded set of types: if variants
are types, then every variant is a different type from
every other variant and there would be no use for a type name.

 This is hard to explain until you 'get it':
there are no variant objects either:

 variant Num [int, long];
 Num x = 1;

Is 'x' a variant object? Nope, its an 'int'. If you wrote the
above code as

 int x =1;

it would be exactly the same effect, no changes in the program
semantics would occur. The object that x refers to in both cases
would occupy 'sizeof int' storage. (For a reasonably smart
compiler :-)

 Now consider

 f(Num a) {
  cout << a;
 };

How much store does 'a' use up? Answer: it depends on
the implementation technique. It might be that it uses up
'sizeof long' and the function is implemented like:

 f(Num a) {
  select(a) {
  type(int x) {  cout << x; }
  type(long y ) { cout << y; }
  }
 }

where you see that 'x' and 'y' are the only *actual* objects
you can refer to, and they have type 'int' and 'long'.

But another perfectly acceptable implementation of the function is

 f(int a) { cout << a; }
 f(long a) { cout << a; }

and there is no way to tell the difference. One might say that
'variants commute with functions' in that a call

 Num n;
 f(n);

might be resolved as

 select(n)
 {
  type(int n) { f(n); }
  type(long n) { f(n); }
 }

in the second implementation. The 'commutability' here is more
like the distributive law in mathematics, it refers to the
transparency of the select statement, which can be
moved between inside the function (first technique) to
outside the function (second technique)

You just cant tell: you cant 'get your hands on a variant'.

Try this view: a variant declaration

 Num a ...

say to the compiler 'a is going to be either an int or
a long, but I wont know which until run time'.

So the compiler says, 'Ok, since you are ignorant, I'll generate
code for *both* cases'. Then, at run time, the appropriate
case is selected and the code for *that* case is executed.

So *no* code can be executed that was not generated for the
static type that the declaration *actually* refers to at
run time .. which is why variants are completely type safe.

And also explains how they are not in themselves types.

Making the tag explicit is not only unnecessary for variants,
its impossible.

But, if you want an explicit tag you can have one, and if you
want to turn a variant into an *actual* type, you can do that
too: encapsulate the variant in a class.

Thats why variants are superior to discriminated unions:
if you have variants and classes, you can have discriminated
unions too.

This is a good thing: you cant change the 'type' of a variant,
because a variant is a reference to an *actual* single normal
type. So you cant do a 'type changing assignment' to a variant.

But you *can* to a dunion. And there is a recently discovered
trick that allows you to use a variant (whose type cannot
be changed after initialisation) to create a discriminated
union (whose type *can* be changed by assignment after initialisation).

If it wasnt for this fact, variants would not be so useful.
I'll leave it to the inventor, Fergus Henderson, to
explain how this is done.

Summary: discriminated unions are a hybrid of variants
and classes.

--
        JOHN (MAX) SKALLER,         INTERNET:maxtal@suphys.physics.su.oz.au
 Maxtal Pty Ltd,      CSERVE:10236.1703
        6 MacKay St ASHFIELD,     Mem: SA IT/9/22,SC22/WG21
        NSW 2131, AUSTRALIA




Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Thu, 20 May 1993 16:51:02 GMT
Raw View
In article <1993May19.143159.1563@rcmcon.com> rmartin@rcmcon.com (Robert Martin) writes:
>maxtal@physics.su.OZ.AU (John Max Skaller) writes:
>>>> Consider:
>>>> struct node {
>>>>  node* left, *right;
>>>>  char op;
>>>> };
>> None of these operations are *intrinsic* to the
>>data structure, so I dont *want* to encapsulate them in the class,
>>because I dont know what I'm going to use the parse tree *for*.
>
>I see your point.   You don't want the nodes of the parse tree to
>"know" what purpose they will be used for.  So let me suggest another
>route.  Separate the data structure from the intent of the data.
>
>class Node
>{
>  public:
>    virtual void Invoke(NodeProcessor&) const = 0;
>};
>
>class BinaryOpNode : public Node
>{
>  public:
>    virtual void Invoke(NodeProcessor& p) const
>    {p.BinOp(*this);}
>};

 Mm. Nice idea.

[]

>class PascalGeneratorNodeProcessor : public NodeProcessor {...};
>class M68KGeneratorNodeProcessor   : public NodeProcessor {...};
>class PostFixPrinterNodeProcessor  : public NodeProcessor {...};

 (BTW: I think you should make all those derivations
virtual: you might want to use 'mixin' node processors :-)

[]

>
>Of course the downside is that the NodeProcessor hierarchy will have
>to undergo lots of changes whenever you add a different kind of Node.
>But this is no worse than changing all the Selects when you add a
>different kind of variant.

 Indeed, its equivalent: this is not a "downside" in the
sense it can be programmed around .. its a simple consequence
of changing the design.

 How can you change the design without changing the design?


--
        JOHN (MAX) SKALLER,         INTERNET:maxtal@suphys.physics.su.oz.au
 Maxtal Pty Ltd,      CSERVE:10236.1703
        6 MacKay St ASHFIELD,     Mem: SA IT/9/22,SC22/WG21
        NSW 2131, AUSTRALIA




Author: fjh@munta.cs.mu.OZ.AU (Fergus James HENDERSON)
Date: Thu, 13 May 1993 08:12:45 GMT
Raw View
u90jl@ecs.ox.ac.uk (Jamie Lokier) writes:

>John Max Skaller <maxtal@physics.su.OZ.AU> writes:
>
>>  They will be considered and ignored in many cases
>> because no alternative has been suggested. And then,
>> because the syntax for accessing RTTI has been deliberately
>> made ugly and inconvenient, we will get an even worse
>> maintenance nightmare than we have now (when at least
>> virtual bases inhibit downcasting)
>>
>>  I suggested that
>>
>>  select(x)
>>  {
>>   type(D1* d1) { .. }
>>   type(D2* d2) { .. }
>>   ..
>>  }
>>
>> was a cleaner syntax for typecasing (and is also safe,
>> whereas a checked cast is not).
>
>Like so many other people, I have found the need for typecasing too.  It
>is possible to do it efficiently without abandoning the ability to
>inherit from classes found in such selection statements.
>
>Here is my suggested syntax for code selection based on class types:
>
>class Base { ... };
>class Foo : Base { ... };
>class Bar : Base { ... };
>class Special_Kind_of_Bar : Bar { ... };
>
>...
>
>void function(Base *object)
>{
>    switch (object) { // `switch (*object)' perhaps?
>
>        case Foo:       // Means Foo or anything derived from it.
>            ...  // In here, object temporarily has type `Foo *'.
>            break;
>
>        case Bar:       // Special_Kind_of_Bar will also get here.
>            ...
>            break;
>
>        default:        // You can have a default if you want.
>            ...  // It is equivalent to `case Base'.
>            break;
>    }
>}

What happens if I omit a "break" statement and drop through?
Or is that illegal?
Your syntax is basically the same as John Skaller's, except that
his is deliberately different from the normal C switch statement
so that (1) it's obvious to the reader that this is a type switch
not a normal switch and (2) to avoid the problem that a switch case
doesn't introduce a new scope.

>This is a clean construct.  It is equivalent, in this case, to declaring
>a virtual function in class Base, and having it called from `function'.
>Sometimes, though, you do not want to declare such a function in the
>base class; it is not really a part of the base class specification.  It
>can also lead to excessive compile-time dependencies, because you have
>to add a new, tiny virtual function to the base class and a few derived
>classes every time you need to do something else with the class.  It can
>also be inefficient at run-time, to call a virtual function, sometimes
>requiring lots of (usually unused) parameters.
>
>Enumerated type tags (even if generated by the compiler) are not the
>answer to this.  In keeping with the OO paradigm, it should not be
>necessary to recompile the code associated with `function' above,
>whenever additional classes are derived from Base or its derivatives.

Well, it depends on what you are trying to do. If you are trying to
emulate some form of discriminated union, then the additional
compilation *is* necessary and desireable. It is essential that the
code associated with `function' above be recompiled when I derive a new
class from Base, so that the compiler can warn me if I forgot to add a
new case to the type switch.

--
Fergus Henderson                     This .signature virus might be
fjh@munta.cs.mu.OZ.AU                getting old, but you still can't
                                     consistently believe it unless you
Linux: Choice of a GNU Generation    copy it to your own .signature file!




Author: tmb@arolla.idiap.ch (Thomas M. Breuel)
Date: 13 May 93 12:30:13
Raw View
>>>>> On Wed, 12 May 1993 13:40:34 GMT, u90jl@ecs.ox.ac.uk (Jamie Lokier) said:
> Enumerated type tags (even if generated by the compiler) are not the
> answer to this.  In keeping with the OO paradigm, it should not be
> necessary to recompile the code associated with `function' above,
> whenever additional classes are derived from Base or its derivatives.

Type switches themselves are not "in keeping with the OO paradigm".
So what? They are still useful, they can be type safe, they can
increase software reuse, and they can reduce the necessity of
modifications to existing classes.  But the compiler can only warn you
about omitted type cases if it sees the type switch at compile time.
Hence, it can be useful to force recompilation in a system that uses
type switches.

Of course, C++ RTTI (to my knowledge) does not permit checking for
omitted type cases in most cases, so the whole issue is pretty
academic.

     Thomas.




Author: u90jl@ecs.ox.ac.uk (Jamie Lokier)
Date: Thu, 13 May 1993 19:53:53 GMT
Raw View
tmb@idiap.ch writes:
> On Wed, 12 May 1993 13:40:34 GMT, u90jl@ecs.ox.ac.uk (Jamie Lokier) said:
> > Enumerated type tags (even if generated by the compiler) are not the
> > answer to this.  In keeping with the OO paradigm, it should not be
> > necessary to recompile the code associated with `function' above,
> > whenever additional classes are derived from Base or its derivatives.
>
> Type switches themselves are not "in keeping with the OO paradigm".

I disagree.  So far as I am aware, one of the points of OO-style
polymorphism is that you can write code which is aware of the properties
of some small part of your larger type-lattice.  The code should not be
aware of the whole lattice extending in some direction, so that you can
happily re-use the code by deriving from the classes the code knows
about.  However, it is reasonable for it to be aware of the local
structure of some part of the lattice.

Some people think that to be truly OO, a piece of code must assume
knowledge of properties of a few base classes, and let derived classes
provide extra functionality which does just what that the piece of code
needed, without the piece of code knowing about it.  It assumes that
your code lets the object itself do everything, and that the code is not
aware of the local derived-class structure around a base class.

I think that view is rather restrictive, and not the way things really
happen anyway.  A piece of code must usually know *something* about a
selection of classes derived from a base.  It is quite reasonable then,
for it to look at an object, and decide what to do depending on which
derived class that object belongs to.  This is fine, and done all the
time.  It is done in a clean way, that the OO purists don't object to,
by providing virtual functions.  The piece of code invokes a virtual
function through an object instance, effectively asking the object
itself to do something on behalf of the piece of code.  Sometimes this
is a good way of doing things.  Sometimes it leads to code dependancies
being in the wrong place.

I have often written a piece of code which needed to do something based
on the type of an object.  It is not interested in the specific type of
an object; it just needs to know in which direction the object is
specialised from some base class.  I implement this by adding yet
another virtual function to the base class definition, and overloading
the definition in the appropriate derived classes.  This is clean.
Unfortunately, that virtual function is only ever called from one place
in my program; namely the piece of code which needed it in the first
place.  Wouldn't it be better, in that case, to move those virtual
function definitions away from the class definitions, to the piece of
code that uses them?  With a hint of optimization, and a bit of syntax,
this leads to the `switch(object)' mechanism I suggested.  I don't see
how this violates data/code encapsulation at all.


-- Jamie Lokier




Author: tmb@arolla.idiap.ch (Thomas M. Breuel)
Date: 14 May 93 00:14:47
Raw View
>>>>> On 13 May 93 19:53:53 GMT, u90jl@ecs.ox.ac.uk (Jamie Lokier) said:
> tmb@idiap.ch writes:
>> On Wed, 12 May 1993 13:40:34 GMT, u90jl@ecs.ox.ac.uk (Jamie Lokier) said:
>> > Enumerated type tags (even if generated by the compiler) are not the
>> > answer to this.  In keeping with the OO paradigm, it should not be
>> > necessary to recompile the code associated with `function' above,
>> > whenever additional classes are derived from Base or its derivatives.
>>
>> Type switches themselves are not "in keeping with the OO paradigm".
>
> I disagree.  So far as I am aware, one of the points of OO-style
> polymorphism is that you can write code which is aware of the properties
> of some small part of your larger type-lattice.

If you view any form of "typecase with default" as "OO-style" (as you
seem to), then I think the term has lost most of its meaning for you.
Any dynamically typed language with type predicates would then be
"object oriented", which seems somewhat nonsensical, given how much
effort designers of dynamically typed languages have put into adding
"object oriented extensions" to their languages.

> I think that view is rather restrictive, and not the way things really
> happen anyway.

I agree completely.  The difference is that you seem to prefer to
broaden the meaning of the term "object oriented", while I prefer to
say that a real-world language by necessity needs support for features
that go beyond the "object oriented" paradigm (even Smalltalk, for
example, adds dynamic typing to its object-oriented approach to data
types).

      Thomas.




Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Fri, 14 May 1993 05:57:52 GMT
Raw View
In article <u90jl.141243.12May1993@ecs.ox.ac.uk> u90jl@ecs.ox.ac.uk (Jamie Lokier) writes:
>John Max Skaller <maxtal@physics.su.OZ.AU> writes:
>
>>  They will be considered and ignored in many cases
>> because no alternative has been suggested. And then,
>> because the syntax for accessing RTTI has been deliberately
>> made ugly and inconvenient, we will get an even worse
>> maintenance nightmare than we have now (when at least
>> virtual bases inhibit downcasting)
>>
>>  I suggested that
>>
>>  select(x)
>>  {
>>   type(D1* d1) { .. }
>>   type(D2* d2) { .. }
>>   ..
>>  }
>>
>> was a cleaner syntax for typecasing (and is also safe,
>> whereas a checked cast is not).
>
>Like so many other people, I have found the need for typecasing too.

 Of course: there is nothing wrong with type casing.
But there is something very very bad with downcasting.

>It
>is possible to do it efficiently without abandoning the ability to
>inherit from classes found in such selection statements.

 I dont understand this comment. Ideally, typecasing
has *absolutely nothing to do with inheritance AT ALL*.

 So there is no reason you cant use or not use inheritance
with a set of classes that you also do good typecasing with.

 Its the other way around: you ar FORCED to abandon
non-inheritance if you use downcasting for typecasing: it
is an invasive technique.
>
>Here is my suggested syntax for code selection based on class types:
>
>
>class Base { ... };
>class Foo : Base { ... };
>class Bar : Base { ... };
>class Special_Kind_of_Bar : Bar { ... };

 This is still downcasting: invasive and contrary to
the intended 'open'ness of base classes.
>
>
>This is a clean construct.  It is equivalent, in this case, to declaring
>a virtual function in class Base, and having it called from `function'.
>Sometimes, though, you do not want to declare such a function in the
>base class; it is not really a part of the base class specification.

 Exactly. But sound principles require it. Cheating by
using a better syntax is still cheating :-)

>It
>can also lead to excessive compile-time dependencies, because you have
>to add a new, tiny virtual function to the base class and a few derived
>classes every time you need to do something else with the class.  It can
>also be inefficient at run-time, to call a virtual function, sometimes
>requiring lots of (usually unused) parameters.

 Thats right and its desirable that this be the case
to discourage abuse of inheritance for typecasing.

 Inheritance is unrelated to type selection, and type
selection should be an entirely different mechanism.

>
>Enumerated type tags (even if generated by the compiler) are not the
>answer to this.  In keeping with the OO paradigm, it should not be
>necessary to recompile the code associated with `function' above,
>whenever additional classes are derived from Base or its derivatives.

 No this is confused IMHO. It IS necessary to recompile
with typecasing, and this *is* in keeping with sound design
principles. Typecasing is the exact opposite of virtual functions.

 And I dont think the magical 'OO paradigm' has
anything to do with typecasing, which can be used in any
procedural language.

 No, let me adjust that thought: in languages like
Smalltalk, the concepts of typeselection and method dispatch
are merged by dynamism. Therefore, people who work
with static languages like C++ but have Stalk backgrounds
may be mislead into thinking that Stalk OO is C++ OO.
>
>I think the answer lies in "virtual jumps".

 Correct. See discussion of "variants" in comp.std.c++.

>The compiler reserves
>entries in the vtable for the appropriate classes, one entry for every
>switch statement involving those classes, which contains the jump
>address of the appropriate case statement.

 Nope, you dont need to do that: the variant techique
has nothing to do with virtual functions, vtables, object orientation,
or anything else like that.

 It has to do with a 'smart union'.

>There is the problem of linking.  Conventional linker technology is
>unable to cope with the mechanism I have suggested.  But then, many
>linkers can't cope directly with static constructors either.
>

 Variants have no such problem, they're not even types.

 I'll post some information to comp.lang.c++.

--
        JOHN (MAX) SKALLER,         INTERNET:maxtal@suphys.physics.su.oz.au
 Maxtal Pty Ltd,      CSERVE:10236.1703
        6 MacKay St ASHFIELD,     Mem: SA IT/9/22,SC22/WG21
        NSW 2131, AUSTRALIA




Author: u90jl@ecs.ox.ac.uk (Jamie Lokier)
Date: Fri, 14 May 1993 17:19:17 GMT
Raw View
In Fergus James HENDERSON <fjh@munta.cs.mu.OZ.AU> writes:

> u90jl@ecs.ox.ac.uk (Jamie Lokier) writes:
> >John Max Skaller <maxtal@physics.su.OZ.AU> writes:
> >
> >>  They will be considered and ignored in many cases
> >> because no alternative has been suggested. And then,
> >> because the syntax for accessing RTTI has been deliberately
> >> made ugly and inconvenient, we will get an even worse
> >> maintenance nightmare than we have now (when at least
> >> virtual bases inhibit downcasting)
> >>
> >>  I suggested that
> >>
> >>  select(x)
> >>  {
> >>   type(D1* d1) { .. }
> >>   type(D2* d2) { .. }
> >>   ..
> >>  }
> >>
> >> was a cleaner syntax for typecasing (and is also safe,
> >> whereas a checked cast is not).
> >
> >Like so many other people, I have found the need for typecasing too.  It
> >is possible to do it efficiently without abandoning the ability to
> >inherit from classes found in such selection statements.
> >
> >Here is my suggested syntax for code selection based on class types:
> >
> >class Base { ... };
> >class Foo : Base { ... };
> >class Bar : Base { ... };
> >class Special_Kind_of_Bar : Bar { ... };
> >
> >...
> >
> >void function(Base *object)
> >{
> >    switch (object) { // `switch (*object)' perhaps?
> >
> >        case Foo:       // Means Foo or anything derived from it.
> >            ...  // In here, object temporarily has type `Foo *'.
> >            break;
> >
> >        case Bar:       // Special_Kind_of_Bar will also get here.
> >            ...
> >            break;
> >
> >        default:        // You can have a default if you want.
> >            ...  // It is equivalent to `case Base'.
> >            break;
> >    }
> >}
>
> What happens if I omit a "break" statement and drop through?
> Or is that illegal?

Details, details...  I never really liked the break statement anyway.
But let's keep the language consistent, eh?

> Your syntax is basically the same as John Skaller's, except that
> his is deliberately different from the normal C switch statement
> so that (1) it's obvious to the reader that this is a type switch
> not a normal switch and (2) to avoid the problem that a switch case
> doesn't introduce a new scope.

No, the semantics are not the same.  In my construct, `case XXX' means
"jump here if the object is of type XXX *or anything derived from it*".
So the `case' actually refers to a potentially infinite set of types,
which the compiler does not know about when it compiles the module.
This retains an important level of abstraction between a particular
switch statement and the class heirarchy it is working with.

Unlike the conevntional `switch' statement, my `cases' are not testing
*equality* of types.  It's more like testing a partial order on them.
Perhaps `switch (typeof(object))' would be more appropriate?

Also quite important:  His syntax introduces quite a lot of new keywords.

For example, suppose you have the following class heirarchy (window
things are always a good example :-):


                          Widget
                            |
               _____________|______________
              |                            |
           Window                        Button
              |                            |
       _______|_______              _______|_______
      |               |            |               |
  Round_Window   Square_Window  Yes_No_Button  Radio_Button


You can meaningfully do this:

{
    Widget *widget = ...;

    switch (widget) {
        case Window:
            widget.map(); // map() is a virtual function of Window.
            break;

        case Button:
            widget.select(); // select() is a virtual function of Button.
            break;
    }
}

-- Jamie Lokier




Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Sat, 15 May 1993 15:21:03 GMT
Raw View
In article <u90jl.180459.14May1993@ecs.ox.ac.uk> u90jl@ecs.ox.ac.uk (Jamie Lokier) writes:
>In Fergus James HENDERSON <fjh@munta.cs.mu.OZ.AU> writes:
>
>> u90jl@ecs.ox.ac.uk (Jamie Lokier) writes:
>> >John Max Skaller <maxtal@physics.su.OZ.AU> writes:
>> >
>> >>  select(x)
>> >>  {
>> >>   type(D1* d1) { .. }
>> >>   type(D2* d2) { .. }
>> >>   ..
>> >>  }
>> >>
>> >> was a cleaner syntax for typecasing (and is also safe,
>> >> whereas a checked cast is not).
>> >
Lokier:
>> >void function(Base *object)
>> >{
>> >    switch (object) { // `switch (*object)' perhaps?
>> >
>> >        case Foo:       // Means Foo or anything derived from it.
>> >            ...  // In here, object temporarily has type `Foo *'.
>> >            break;
>> >
>> >        case Bar:       // Special_Kind_of_Bar will also get here.
>> >            ...
>> >            break;
>> >
>> >        default:        // You can have a default if you want.
>> >            ...  // It is equivalent to `case Base'.
>> >            break;
>> >    }
>> >}
>>
>> What happens if I omit a "break" statement and drop through?
>> Or is that illegal?
>
>Details, details...  I never really liked the break statement anyway.
>But let's keep the language consistent, eh?
>
>> Your syntax is basically the same as John Skaller's, except that
>> his is deliberately different from the normal C switch statement
>> so that (1) it's obvious to the reader that this is a type switch
>> not a normal switch and (2) to avoid the problem that a switch case
>> doesn't introduce a new scope.
>
>No, the semantics are not the same.  In my construct, `case XXX' means
>"jump here if the object is of type XXX *or anything derived from it*".
>So the `case' actually refers to a potentially infinite set of types,
>which the compiler does not know about when it compiles the module.
>This retains an important level of abstraction between a particular
>switch statement and the class heirarchy it is working with.
>
>Unlike the conevntional `switch' statement, my `cases' are not testing
>*equality* of types.  It's more like testing a partial order on them.
>Perhaps `switch (typeof(object))' would be more appropriate?

 I'm confused. What gave you the idea that my syntax precluded
that? In fact, my syntax does more than that again, I just didnt tell
you about it.

 I allow *arbitrary* conversions because my syntax is
defined to be equivalent to function call. Not only do I allow
derived type pointers to convert to base class pointers, I allow
any conversion.

 I also allow multiple arguments:

 select(x,y)
 {
  type(X x, Y *y) { ...}
  type(X x, Z *z) { ...}
  type(X x, ...) { ...}
 }

Now you know that its function call you can see why I chose this
syntax and not the switch. Thise 'type( ..) { ..}' are literally
functions.. exactly like 'catch' clauses.
>
>Also quite important:  His syntax introduces quite a lot of new keywords.

 Bull. This is a trivial point, and it introduces exactly
two new keywords, 'select' and 'type'. (Actually, something
else woudl be chosen since 'select' is a common function on
some systems... but thats beside the point)
>
>For example, suppose you have the following class heirarchy (window
>things are always a good example :-):
>
>
>                          Widget
>                            |
>               _____________|______________
>              |                            |
>           Window                        Button
>              |                            |
>       _______|_______              _______|_______
>      |               |            |               |
>  Round_Window   Square_Window  Yes_No_Button  Radio_Button
>
>
>You can meaningfully do this:
>
>{
>    Widget *widget = ...;

 You mean reference here, or to use -> below
>
>    switch (widget) {
>        case Window:
>            widget.map(); // map() is a virtual function of Window.
>            break;
>
>        case Button:
>            widget.select(); // select() is a virtual function of Button.
>            break;
>    }
>}
>

 So? I can meaningfully do this:

 select(widget1, widget2)
 {
  type(Window& w1, Window& w2) {w1.map(); w2.map(); }
  type(Button& b1, ...) { b1.select(); }
 }

You could easily modify your switch to allow multiple arguments.
What you cant do is support initialisation, but thats
exactly what your code above does, now I think about it:
your switch is actually illegal.

(Its illegal to 'jump' around an initialisation. But your
switch does that because your 'widget' in each case has a different
type than the original type .. you *have* to actually convert
a pointer to a new one to do a cast you know.)

I think my syntax is ideal, especially because I saw that someone
else invented exactly the same syntax.

I advocate my syntax ... but not for downcasting, but use with
variants (sets of types)

--
        JOHN (MAX) SKALLER,         INTERNET:maxtal@suphys.physics.su.oz.au
 Maxtal Pty Ltd,      CSERVE:10236.1703
        6 MacKay St ASHFIELD,     Mem: SA IT/9/22,SC22/WG21
        NSW 2131, AUSTRALIA




Author: rmartin@uunet.uu.net!rcm (Robert Martin)
Date: Mon, 17 May 1993 03:25:04 GMT
Raw View
u90jl@ecs.ox.ac.uk (Jamie Lokier) writes:

>Like so many other people, I have found the need for typecasing too.  It
>is possible to do it efficiently without abandoning the ability to
>inherit from classes found in such selection statements.

>Here is my suggested syntax for code selection based on class types:


>class Base { ... };
>class Foo : Base { ... };
>class Bar : Base { ... };
>class Special_Kind_of_Bar : Bar { ... };

>void function(Base *object)
>{
>    switch (object) { // `switch (*object)' perhaps?
>        case Foo:       // Means Foo or anything derived from it.
>           ...   // In here, object temporarily has type `Foo *'.
>            break;
>        case Bar:       // Special_Kind_of_Bar will also get here.
>            ...
>            break;
>        default:        // You can have a default if you want.
>            ...  // It is equivalent to `case Base'.
>            break;
>    }
>}

>This is a clean construct.

I disagree.  Such structures should be avoided where possible.  (And
it is almost always possible).  Such structures will be difficult to
maintain, and will be error prone.  This is because when new classes
are derived from base, you must hunt for all the switch statements
that test for derivatives of base, and add the appropriate code to
them.  This defeats one of the great advantages of OO.


--
Robert Martin      | Design Consulting       |Training courses offered:
R.C.M. Consulting  | rcm!rmartin@uunet.UU.NET| Object Oriented Analysis
2080 Cranbrook Rd. | Tel: (708) 918-1004     | Object Oriented Design
Green Oaks IL 60048| Fax: (708) 918-1023     | C++




Author: rmartin@uunet.uu.net!rcm (Robert Martin)
Date: Mon, 17 May 1993 03:29:09 GMT
Raw View
tmb@arolla.idiap.ch (Thomas M. Breuel) writes:

>Type switches themselves are not "in keeping with the OO paradigm".
>So what? They are still useful, they can be type safe, they can
>increase software reuse, and they can reduce the necessity of
>modifications to existing classes.  But the compiler can only warn you
>about omitted type cases if it sees the type switch at compile time.
>Hence, it can be useful to force recompilation in a system that uses
>type switches.

While I agree that there is a time and place for type switches, I
would not go so far as to say that they are generally useful.  They
should be strenuously avoided; and used only at great need.  It will
pay off in maintenance costs and robustness of design to find a way to
use virtual functions instead of type switches.

--
Robert Martin      | Design Consulting       |Training courses offered:
R.C.M. Consulting  | rcm!rmartin@uunet.UU.NET| Object Oriented Analysis
2080 Cranbrook Rd. | Tel: (708) 918-1004     | Object Oriented Design
Green Oaks IL 60048| Fax: (708) 918-1023     | C++




Author: u90jl@ecs.ox.ac.uk (Jamie Lokier)
Date: Wed, 12 May 1993 13:40:34 GMT
Raw View
John Max Skaller <maxtal@physics.su.OZ.AU> writes:

>  They will be considered and ignored in many cases
> because no alternative has been suggested. And then,
> because the syntax for accessing RTTI has been deliberately
> made ugly and inconvenient, we will get an even worse
> maintenance nightmare than we have now (when at least
> virtual bases inhibit downcasting)
>
>  I suggested that
>
>  select(x)
>  {
>   type(D1* d1) { .. }
>   type(D2* d2) { .. }
>   ..
>  }
>
> was a cleaner syntax for typecasing (and is also safe,
> whereas a checked cast is not).

Like so many other people, I have found the need for typecasing too.  It
is possible to do it efficiently without abandoning the ability to
inherit from classes found in such selection statements.

Here is my suggested syntax for code selection based on class types:


class Base { ... };
class Foo : Base { ... };
class Bar : Base { ... };
class Special_Kind_of_Bar : Bar { ... };

...

void function(Base *object)
{
    switch (object) { // `switch (*object)' perhaps?

        case Foo:       // Means Foo or anything derived from it.
            ...  // In here, object temporarily has type `Foo *'.
            break;

        case Bar:       // Special_Kind_of_Bar will also get here.
            ...
            break;

        default:        // You can have a default if you want.
            ...  // It is equivalent to `case Base'.
            break;
    }
}


This is a clean construct.  It is equivalent, in this case, to declaring
a virtual function in class Base, and having it called from `function'.
Sometimes, though, you do not want to declare such a function in the
base class; it is not really a part of the base class specification.  It
can also lead to excessive compile-time dependencies, because you have
to add a new, tiny virtual function to the base class and a few derived
classes every time you need to do something else with the class.  It can
also be inefficient at run-time, to call a virtual function, sometimes
requiring lots of (usually unused) parameters.

Enumerated type tags (even if generated by the compiler) are not the
answer to this.  In keeping with the OO paradigm, it should not be
necessary to recompile the code associated with `function' above,
whenever additional classes are derived from Base or its derivatives.

Compiler-generated virtual functions are not the answer either.
Although they would be a clean way to implement the construct, they can
incur excessive run-time overhead if many parameters need to be passed
to the virtual function (the local variables, for example, in the
function enclosing the switch statement).

I think the answer lies in "virtual jumps".  The compiler reserves
entries in the vtable for the appropriate classes, one entry for every
switch statement involving those classes, which contains the jump
address of the appropriate case statement.  Of course, this is merely an
implementation detail, and needn't be followed by any particular
compiler.  However, it demonstrates that such a mechanism needn't cost
much at run-time.  The only cost is the space in the vtables; that isn't
really a problem.

There is the problem of linking.  Conventional linker technology is
unable to cope with the mechanism I have suggested.  But then, many
linkers can't cope directly with static constructors either.


-- Jamie Lokier
mail: u90jl@ecs.oxford.ac.uk