dynamic_cast is ugly!

H

H. S. Lahman

Responding to queisser...
Redefine "external" and "subsystem interface" just a little bit and
everyone's happy. I think traversing a collection and only acting on certain
types is preferable to having the same object in two collections, just to
avoid dynamic_cast or, even worse, forcing functionality with do-nothing
defaults into the superclass (although I didn't hear anyone advocating
that.)

The OO paradigm manages access through relationship instantiation.
That's why relationships are instantiated at the object level rather
than at the class level. It is also why one is encouraged to have
multiple relationships between the same classes to support different
collaborations (e.g., different relationship roles in UML).

One reason for doing that is to separate the concerns of *who*
participates in collaboration from the concerns of *when* to
collaborate. That's because the business rules and policies of who
collaborates are usually quite different than those that govern when to
collaborate, so they need to be isolated and encapsulated in their own
methods that are logically "owned" by the right object abstraction.

Thus the client in an OO collaboration should normally only need to know
when to collaborate, not who to collaborate with. The client has faith
that whoever is on the other end of the relationship path is *always*
the right object. Even in the rare situations where the client object
also has the responsibility to determine who to collaborate with in the
solution context, that responsibility would be encapsulated in a
different method.

The name of the game in OO development is robustness in the face of
volatile requirements. The application will *always* be more robust if
one separates the concerns of who vs. when during collaboration, whether
that is convenient or not. If one limits dynamic_cast to subsystem
interface dispatch, then the subsystem implementation will be more
robust because whatever one did to avoid it will be tacitly separating
those concerns.

[FWIW, I agree with the no-op position. If the behavior is a no-op for
all members of a descendant leaf subclass in all solution contexts, then
the behavior is defined at the wrong level in the generalization.
Objects have responsibilities to know things and to do things. They do
not have responsibilities to not do things. By that logic every object
would have a no-op method for every behavior in every other object
because it is not supposed to do anything for those other object
responsibilities. IOW, if the object is not supposed to do something,
then it doesn't have a responsibility to do it. But let's not go there.]
I think most people will strive to avoid type-testing elements in
heterogenous collections but when the time comes where you need it most
design-arounds end up being dynamic_cast in disguise. Why not just embrace
it - you know you want to.

Because if one gets to the point where one thinks one needs it, it is a
sure sign that one has somehow screwed up the problem space abstraction
in OOA/D. B-)

<aside>
Alas, this sort of argument is not persuasive to me in general. That's
because I grew up before Djikstra when people thought GoTos were a boon
and FORTRAN's assigned GoTo and COBOL's ALTER statements were even
better. I was there when the view shifted to "sometimes you need it" and
that seemed persuasive. But then I programmed in a language, BLISS, that
didn't even have a GoTo construct and I found I never missed it. Nor
have I needed it since using languages that did have the construct. And
about the same time programming managers were making using assigned GoTo
or ALTER grounds for summary dismissal, and nobody missed them either.

No offense intended but... IME, when developers agree that a practice is
something to be avoided but that there are exceptions, those exceptions
turn out to mean they just haven't looked hard enough for an
alternative. It is usually a very safe bet that OOA/D methodology
designers have considered the problem and have provided the means to
deal with it. That might means might not be compact and might require
some extra keystrokes, but compactness and keystrokes aren't an issue
for OO development; otherwise we would all be doing functional programming.
</aside>



--
There is nothing wrong with me that could
not be cured by a capful of Drano.

H. S. Lahman
(e-mail address removed)
Pathfinder Solutions
http://www.pathfindermda.com
blog: http://pathfinderpeople.blogs.com/hslahman
"Model-Based Translation: The Next Step in Agile Development". Email
(e-mail address removed) for your copy.
Pathfinder is hiring:
http://www.pathfindermda.com/about_us/careers_pos3.php.
(888)OOA-PATH
 
N

Nick Keighley

Because the middleware didn't provide it.  Because the
middleware doesn't care about the type---it's just there to
ensure communication.

Because I have no control over what version of the software the
client is running.  So I have to be prepared to handle two
different versions.

ah, this sounds like Lahman's Exception:

"dynamic_cast exists in C++ because there is no other facility for
dispatching from an external stream of objects of different types in
a
subsystem interface. But that is the only place that it should be
used."

Which is where I usually allow it.

The order of the requests?



Who knows?  But it doesn't seem so unreasonable to me that
different operations may still require ordering between them.


Maybe.

My point is just that there are a number of different tools
available, and no one tool is always the right one.



The fact that the middleware doesn't know (and isn't supposed to
know) the services provided by the higher levels.

In large projects, the software tends to be structured very much
like network protocols.  The IP layer doesn't know beans about
NNTP, and shouldn't.  Similarly, the various layers that connect
the different components of a large application shouldn't know
beans about the services provided by those components.

that sounds a very resonable place for a dynamic cast.
 
M

Matthias Buelow

James said:
That depends on whether you want your program to work reliably
or not.

Well, reliable programs aren't written by the compiler; programs become
more reliable if the programmers working on them are able to understand
them and adapt them to changing requirements without having to introduce
gruesome ad-hoc hacks or having to redesign the entire thing (which
usually is prohibitively expensive, and therefore not done.) From my
experience, programs written in a "dynamically" typed language are
simply more malleable and also more concise and hence easier to
understand. Add to that interactive development (like in Lisp, or other
languages that provide an interactive toplevel), and the whole
development process shows a much faster turnaround because one doesn't
have to wait for the compiler and a new test run all the time. At least
that's my experience. Faster turnaround means more testing of new ideas,
and more testing of code and debugging; in the end resulting in better code.
Violate the type system (static or dynamic), and the
code doesn't work.

The code also won't work for any kind of logics errors, which no
compiler could guard against. There's simply no substitute for testing.
After testing, runtime type errors seem to be rather rare and
unproblematic compared to more serious program flaws which are sometimes
hard to detect, and even harder if the program logic is further
obfuscated by having to accomodate a complicated type hierarchy.
It's a burden on the programmer, yes. It means that he actually
has to design what he writes, and document it up front, in a
standard way that even a compiler can understand.

Not every application can be properly specified before programming
starts, in some cases, what the application actually ought to do in
detail might be unclear in the beginning and only become manifest during
an exploratory development process, and in addition, many applications
change quite a lot over their lifetime. A complete up-front design is
often not possible, or even desirable. I think a lot more software
suffers from a too rigid specification and implementation process than
from a too loose one. Unfortunately, exploratory development is a recipe
for disaster with inflexible languages like C++ or Java because you'll
end up with a horrible type mess. IMHO, using a dynamically typed
language can make a difference here. The idea is not to restrict the
programmer at all in what he/she can do, while giving him/her as much
expressive power as possible. C++ is quite powerful (albeit in a
sometimes very awkward and verbose way) but also restrictive, while Java
only restricts and gives very little in the way of expressive strength.
Languages like ML have a more powerful type system that is somewhat more
expressive than the primitive ones of C++ and Java but the main problem
remains: Once the program stands, no matter how beautifully designed it
is, it is hard to make substantial changes to it because of the rigidity
of the type structure.
 
D

Daniel T.

James Kanze said:
I think you're missing the point. The base class interface is
only designed for a specific use.

I think *you* are missing the point. The base class interface is
designed for a specific use, and we are talking about code that *uses*
the base class interface. A container that contains objects of the
base class, and requesters that request objects of the base class.

The fact that once you receive the base class object you are querying
it for what *other* functionality the object may have is the problem.
 
H

H. S. Lahman

Responding to Nieminen...
In my particular case I am (usually) keeping objects of a certain type
in a container of that type, and I access this container directly when
it's possible to do so (eg. to perform an action on some or all the
objects of that type, as long as this action is independent of any other
objects in the system).

The problem is that not all actions I need to perform can be applied
to objects independently of other objects. In particular, sometimes some
actions need to be performed in a certain order. The two problems are
that the objects in the object-specific container might not be in this
order already, and secondly, even if they were, some actions have to be
performed to all existing objects (regardless of their type) in a strict
order, and cannot be performed on a per-type basis.

This is actually an easy problem to solve with multiple homogeneous
collections without any dynamic_cast.

Ordering is a natural responsibility of collections and the natural
approach is for a getNext collection interface to provide elements in
the correct order from its implementation. So the ordering is not
directly a concern of clients processing the collection elements.
Consequently the ordering is only related to the element type through
problem space serendipity. Hold that thought for a moment...

If I understand the problem correctly you have:

[ClassA]
|*
| accesses
|
| R1 <<ordered>>
|
| 1
[Client]
+ actionForA()
+ actionForB()
+ actionForAll()
| 1
|
| R2 <<ordered>>
|
| accesses
| *
[ClassB]

where the relevant collections implement the R1 and R2 associations. The
ordering of the R1 collection is suitable for actionForA() that needs to
process members of [ClassA]. Similarly, the ordering of the R1
collection is suitable for actionForB() that needs to process members of
[ClassB].

So the tricky part is dealing with the ordering for actionForAll(). The
simplest case is that ordering of R1 and R2 is the same and that
actionForAll() just needs to interleave the results of getNext from the
two collections. I'll deal with such interleaving in the discussion of
(2) below.

A trickier problem is that the R1 and R2 ordering are not the same or
that actionForAll() requires some completely different ordering. There
is no restriction on the R1 and R2 collections that they must provide
only one ordering. In practice it is usually more efficient for a single
collection class to managing multiple orderings of the same set.

So all we need is to provide an additional interface to the R1 and R2
collections to present elements in a different order (e.g.,
getNextOrder1() and getNextOrder2()), where getNextOrder1() provides the
ordering for access by actionForA() or actionForB() while
getNextOrder2() provides the ordering for access by actionForAll(). Now
each actionFor... method employs the correct interface fro dealing with
each collection. In the case of actionForAll(), we are back to
interleaving again, as above.

Note that it is quite fair for the client to utilize a particular
interface for ordering because ordering is a *collection* responsibility
and a characteristic of the relationship that has nothing directly to do
with what classes the collection objects belong to.
I'm not exactly sure what this means, and how it is radically
different from using dynamic_cast. (It sounds to me like dynamic_cast
would simply have been replaced with some kind of checking of the 'type'
inside those tuples. If this is so, then it would simply make the whole
implementation more complicated with no real benefit.)

This becomes relevant for the interleaving. There has to be some
attribute that is a context variable for the ordering or else the
collection sets could not be ordered in the first place. For the sake of
example, let's assume all the objects have a 'size' attribute and
actionForA() and actionForB() require ordering in ascending order of
size, while actionForAll() requires descending order of size.

It is quite fair for actionForAll() to obtain the next element from each
collection and compare sizes to determine which one of the two to
process next. actionForAll() then does getNext() on the appropriate
collection to replace the element just processed, and continues the
interleaving in this manner.

This processing does not directly depend on the object types. It just
depends on the 'size' attribute and the fact that the R1 and R2
collections provide the correct order for individual elements when taken
one-at-a-time. So 'size' is a context variable that is synchronized with
the R1 and R2 collection ordering to make interleaving simple for
actionForAll().

The only way the object type comes into the picture is when
actionForAll() actually processes one of the objects returned from
getNext. But that object is *always* the right type for the association
that was navigated. That is, when actionForAll() navigates R1, it always
gets back a ClassA object. So the type safety issues are explicitly
associated with the relationship rather than type checking of random
objects.

[Of course somebody has to ensure that only [ClassA] objects are put
into the R1 collection. But that is a different trade union from the
responsibilities of [Client] that drive the collaboration. So we
separate the concerns and absolve [Client] from any dependence on those
sorts of type issues. So [Client] can do its thing with whoever is there
and navigating separate relationships ensures type safety.]

In this case the problem context has been abstracted in two quite
different ways that play together. One way is the context variable,
size, that is used to compare individual elements and order the R1 and
R2 collections. The other way is through abstracting separate
relationship collections that are ordered in a particular fashion to
suite the needs of the collaboration between [Client] and both [ClassA]
and [ClassB].

The value of those separate collections is that they unambiguously tie
type safety to static structure. The cost of doing that is really not
very much, other than the static declaration boilerplate. The members of
[ClassA] and [ClassB] are already peer objects of [Client]. In OOA/D we
would naturally have separate relationships between them, as my Class
Model fragment above showed.

That is, in OOA/D relationships are always between individual classes.
So using a heterogeneous collection class is actually a rather unnatural
way to solve this problem from an OOA/D perspective. In UML the closest
one comes is an n-ary relationship where the implementation collection
class *might* be heterogeous.

[And n-ary associations are not necessary to OOA/D. In fact, in the MDA
profile I use for OOA/D the UML subset does not include n-ary
associations, so there is no direct way to even represent such a
collection. Yet, as a translationist, my UML models are unambiguous
specifications for a full code generator that can target C++, C, Java,
or even MS Word documentation.]

--
There is nothing wrong with me that could
not be cured by a capful of Drano.

H. S. Lahman
(e-mail address removed)
Pathfinder Solutions
http://www.pathfindermda.com
blog: http://pathfinderpeople.blogs.com/hslahman
"Model-Based Translation: The Next Step in Agile Development". Email
(e-mail address removed) for your copy.
Pathfinder is hiring:
http://www.pathfindermda.com/about_us/careers_pos3.php.
(888)OOA-PATH
 
H

H. S. Lahman

Responding to Keighley...
the Shape collection also holds a field with type info (yuck!)



for instance? Could you give a simple concrete example?

See the example at the end of my most recent response (today) to the OP.

As I indicated to Daniel T., my main point with the caveat here is that
including specific type information with the collection is only
marginally better than the dynamic_cast kludge. One really shouldn't do
either one.


--
There is nothing wrong with me that could
not be cured by a capful of Drano.

H. S. Lahman
(e-mail address removed)
Pathfinder Solutions
http://www.pathfindermda.com
blog: http://pathfinderpeople.blogs.com/hslahman
"Model-Based Translation: The Next Step in Agile Development". Email
(e-mail address removed) for your copy.
Pathfinder is hiring:
http://www.pathfindermda.com/about_us/careers_pos3.php.
(888)OOA-PATH
 
D

Daniel T.

It's not really that you need dynamic cast to *know* if the shape is a
Square. In this case you need it if you want to *do* something to the
object if it's a Square, and this operation is not supported by Shape,
only by Square.

And therein lies the problem. "you want to *do* something to the
object if it's a Square". If you find yourself in that situation, the
design has already slid downhill. IMHO.
 
R

Richard Herring

H. S. Lahman said:
Responding to Nieminen...


This is actually an easy problem to solve with multiple homogeneous
collections without any dynamic_cast.

Ordering is a natural responsibility of collections and the natural
approach is for a getNext collection interface to provide elements in
the correct order from its implementation. So the ordering is not
======================
directly a concern of clients processing the collection elements.
================================================================
Consequently the ordering is only related to the element type through
problem space serendipity. Hold that thought for a moment...

If I understand the problem correctly you have:

[ClassA]
|*
| accesses
|
| R1 <<ordered>>
|
| 1
[Client]
+ actionForA()
+ actionForB()
+ actionForAll()
| 1
|
| R2 <<ordered>>
|
| accesses
| *
[ClassB]

where the relevant collections implement the R1 and R2 associations.
The ordering of the R1 collection is suitable for actionForA() that
needs to process members of [ClassA]. Similarly, the ordering of the R1
collection is suitable for actionForB() that needs to process members
of [ClassB].

So the tricky part is dealing with the ordering for actionForAll(). The
simplest case is that ordering of R1 and R2 is the same and that
actionForAll() just needs to interleave the results of getNext from the
two collections. I'll deal with such interleaving in the discussion of
(2) below.
[snip trickier problem, not relevant to my point]
Note that it is quite fair for the client to utilize a particular
interface for ordering because ordering is a *collection*
==========================
responsibility and a characteristic of the relationship that has ==============
nothing directly to do with what classes the collection objects belong
to.
[...]


This becomes relevant for the interleaving. There has to be some
attribute that is a context variable for the ordering or else the
collection sets could not be ordered in the first place. For the sake
of example, let's assume all the objects have a 'size' attribute and
actionForA() and actionForB() require ordering in ascending order of
size, while actionForAll() requires descending order of size.

It is quite fair !
for actionForAll() to obtain the next element from each collection and
compare sizes to determine which one of the two to process next.
actionForAll() then does getNext() on the appropriate collection to
replace the element just processed, and continues the interleaving in
this manner.

This processing does not directly depend on the object types. It just
depends on the 'size' attribute and the fact that the R1 and R2
collections provide the correct order for individual elements when
taken one-at-a-time. So 'size' is a context variable that is
synchronized with the R1 and R2 collection ordering to make
interleaving simple for actionForAll().

So now you have to expose that attribute, and the client has to have
access to the ordering relation which was previously encapsulated in the
collection.

And if the number of collections increases beyond two, that "simple
interleaving" doesn't scale well, as it starts to look more and more
like a complete (partial) sort, so really the client, not the
collection, is now responsible for the ordering.

An interesting tradeoff.
 
J

James Kanze

I think *you* are missing the point. The base class interface
is designed for a specific use, and we are talking about code
that *uses* the base class interface. A container that
contains objects of the base class, and requesters that
request objects of the base class.

And that's the point you seem to be missing. We're talking
about code which uses a specific interface, and other code which
produces objects which have that specific interface.

And in between them, there is a transport layer which doesn't
want to know about the details of that user interface, since
it's transporting between a lot of different suppliers and
consumers.
The fact that once you receive the base class object you are
querying it for what *other* functionality the object may have
is the problem.

The fact is that once you receive an object through the
transport layer, you have to query, since things do go wrong,
and you can end up with an object intended for someone else.
 
J

Jeff Schwab

Good discussion. I have a perhaps naive question directed to any and
all in this thread. If you have to work with a hierarchy of classes
that extend the base with their own methods, would it be reasonable to
create a parallel adapter hierarchy with no-op methods on classes that
have no "real" implementation of the individual leaf methods? Or does
this just mask the original design problem with a fat interface?

It depends, but usually the latter IMO. I know that some of the
experienced regulars like the technique, but I personally consider it an
anti-pattern in most cases. The one sort-of exception I would make is
for a policy-based design in which "do-nothing" policies are acceptable.
 
D

Daniel T.

James Kanze said:
And that's the point you seem to be missing. We're talking
about code which uses a specific interface, and other code which
produces objects which have that specific interface.

And in between them, there is a transport layer which doesn't
want to know about the details of that user interface, since
it's transporting between a lot of different suppliers and
consumers.

Right, I understand that. You are talking about a poor design. As I have
said many times, if you have a poor design, whether its as you describe
above or whether it is because all objects are derived from Object and
held in Object*s, you must use dynamic_cast.
The fact is that once you receive an object through the
transport layer, you have to query, since things do go wrong,
and you can end up with an object intended for someone else.

Right, a poor design.
 
D

Daniel T.

James Kanze said:
Agreed. You tell it to do something. In practice, however,
objects may pass through many layers of intermediate software,
which knows nothing about (or should know nothing about) what
services the provider actually offers, and what services the
consumer needs. In practice, however, real software often has
to deal with different versions: you must query whether your
partner supports some new functionality, and be prepared to use
an alternate strategy if it doesn't. Those are, IMHO, two cases
where dynamic_cast is called for---in the first case, it
actually helps design (by encapsulating the functionality at the
relevant level).

That may be the practice in poor designs, but it is not the general case.
 
N

Nick Keighley

in message



Oh?
How, then...? :
(Console app.)

for(;;) {
//no exit condition is programmable.
//live, *vital*, user interaction via keyboard.
//check for a keypress (ch).
switch(ch) {
//case
//case
case: 'q' : goto end;
//case: }
end:
return 0;

}

your layout is shit

do
{
check_for_keypress (&ch);

switch(ch)
{
case 'a':
do_A()
break;

case 'b':
do_B()
break;
}
} while (ch != 'q');
 
D

Daniel T.

Nick Keighley said:
so if you had two "collections" or streams of objects or whatever
then you'd have two different producers. One "ordered" and one "all
objects"?

That is one of many ways to handle it, but the fundamental question is
still there; what is it you are wanting to notify all the Squares of
that you want to keep it secret from the rest of the Shapes?
 
J

Juha Nieminen

Daniel said:
And therein lies the problem. "you want to *do* something to the
object if it's a Square". If you find yourself in that situation, the
design has already slid downhill. IMHO.

And what is the alternative you propose?

I can only think of one possibility: Clutter the 'Shape' base class
with Square-specific virtual functions. This defies all good OO design.

(Note that the circles and squares is just an *example*. There are
similar situations where it indeed makes a lot of sense, and moreover
it's mandatory, to be able to perform some type-specific operations on
objects of that type, and for example the order (or other such
attribute) in which these operations are performed is significant.)
 
D

Daniel T.

Matthias Buelow said:
Well, reliable programs aren't written by the compiler; programs become
more reliable if the programmers working on them are able to understand
them and adapt them to changing requirements without having to introduce
gruesome ad-hoc hacks or having to redesign the entire thing (which
usually is prohibitively expensive, and therefore not done.)

I'll throw in my 2p. just because. :)

The good news, and bad news, with a dynamic-typed language, is that less
of the design is actually written out in code. Because of this, the code
can be written faster, and the the code is harder to figure out after it
is written.

In other words, I think you are both right. :)
 
J

James Kanze

Right, I understand that. You are talking about a poor design.

You've got it backwards. Exposing such details to the transport
layer would be very poor design, and a maintenance nightmare.
As I have said many times, if you have a poor design, whether
its as you describe above or whether it is because all objects
are derived from Object and held in Object*s, you must use
dynamic_cast.
Right, a poor design.

Reality is a poor design?

I'd say that robustness is an important property of a good
design. Being able to continue functionning when some network
component fails is an essential requirement of almost all of the
software I work on.
 
D

Daniel T.

Juha Nieminen said:
And what is the alternative you propose?

Don't try to *do* things to objects. Objects are supposed to do for
themselves, clients *notify* the objects of things, they don't order
them around.
I can only think of one possibility: Clutter the 'Shape' base class
with Square-specific virtual functions. This defies all good OO
design.

What is it you are trying to tell squares that you don't want any other
shapes to know about? Why the big secret?
 
J

James Kanze

That is one of many ways to handle it, but the fundamental
question is still there; what is it you are wanting to notify
all the Squares of that you want to keep it secret from the
rest of the Shapes?

You've got it backwards. Why do you want to encumber all the
rest of the Shapes with something that is only relevant to
Squares?
 
D

Dmitry A. Kazakov

And what is the alternative you propose?

In order to solve this, the container shall be of a dynamically constrained
type, which templates cannot deliver.
I can only think of one possibility: Clutter the 'Shape' base class
with Square-specific virtual functions. This defies all good OO design.

The design should consider how the container of shapes gets constrained.
The problem is that a choice of templates as in your example limits
constraints to be static. So a container of Squares is either of another
type or else of exactly same type as the container of Shapes.

This design is bad and there is nothing to do about it, rather than to
patch it by 1) making 'Shape' fat or by 2) applying dynamic casts.

A third alternative would be to convert std::vector<Shape*> to
std::vector<Square*>. What is the least evil depends on each concrete
situation.

(BTW, using pointers is also bad design. The language should allow
construction of containers of polymorphic elements.)
(Note that the circles and squares is just an *example*. There are
similar situations where it indeed makes a lot of sense, and moreover
it's mandatory, to be able to perform some type-specific operations on
objects of that type, and for example the order (or other such
attribute) in which these operations are performed is significant.)

Certainly there are lots of situations where templates (static
polymorphism, macros, call it as you like) come short.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,997
Messages
2,570,240
Members
46,830
Latest member
HeleneMull

Latest Threads

Top