Newsgroups: comp.std.c++
Path: utzoo!utgpu!craig
From: craig@gpu.utcs.utoronto.ca (Craig Hubley)
Subject: Re: Overloading vs. virtual functions
Message-ID: <1991Mar3.003937.17389@gpu.utcs.utoronto.ca>
Organization: Craig Hubley & Associates
References: <1991Feb19.051123.5198@gpu.utcs.utoronto.ca> <27C2D4B8.3AD3@tct.uucp> <1991Feb25.201923.14554@gpu.utcs.utoronto.ca> <27CD13BF.64D1@tct.uucp>
Date: Sun, 3 Mar 1991 00:39:37 GMT

In article <27CD13BF.64D1@tct.uucp> chip@tct.uucp (Chip Salzenberg) writes:
>According to craig@gpu.utcs.utoronto.ca (Craig Hubley):
>>In article <27C2D4B8.3AD3@tct.uucp> chip@tct.uucp (Chip Salzenberg) writes:
>>Sometimes you must/can be aware of the [compile-time/run-time] seam.
>
>In fact, I find it vital to keep ``the seam'' clearly in mind at all
>times.  Perhaps some find this distinction irritating, and wish to
>forget it.  I don't.

I would guess you are in a minority, but that is only a guess.

>>You can remove the distinction [between compile-time and run-time binding]
                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This is not what I said or meant to say.  I am describing a situation in which
the decision which of two functions is called is visible to the producer but
invisible to the consumer.  Whether that decision is made based on information
available at compile-time, or at run-time (i.e. whether it is space- or time-
multiplexed :)) is irrelevant.

>>in many cases without removing the control.  Take overloading:  if I provide
>>a function that accepts both Date("Feb.25/91") and Date(91, 2, 25) ...
>
>I confess that the point of the Date() example escapes me.  It
>describes only compile-time binding (of constructors, I assume, though
>it hardly matters) and seems irrelevant to virtual functions.

You seem to think compile/run is the only distinction that matters in runtime
terms.  In fact it doesn't matter a damn.  I can write two functions
with *vastly* different runtime consequences (e.g. parsing a string takes 
much more time than moving integers around) and if I do not provide the source
to these to the programer using them (which normally I won't), he/she/it must
live with the results.  There is no way to tell from simple inspection of the
code that Date(char*) will take much longer to return than Date(int,int,int).

These functions are called using an identical signature format.  That is, 
either signature could invoke the time-consuming function.  Having a single
(overloaded function) syntax to invoke multiple functions, or two syntaxes
(virtual vs. "normal" functions) changes nothing.  Having the decision which
function to call made at compile-time or run-time changes nothing.

In all cases, the programmer producing the code must provide and document its
behavior.  This may involve the use of virtuals, overloads, conversions, or
all three, but it can't be assumed to involve handing over source code.  This
is where you err in thinking that you gain "control" by forcing programmers
to differentiate compile-time from run-time binding.  What they do will be
visible to no-one but themselves.

>>[initializations can be optimized away, if they only use...]
>>static arguments that have no (derived types AND virtual functions)"
>
>Of course.  But constant expression folding was state-of-the-art in
>the 60's, and it has zero relevance to OOP typing issues.

It has lots of relevance to efficiency, and that's what you seemed to be
worried about.  Strong typing can narrow the resolution of some functions 
enough that no processing needs to take place at all.

>>The choice of calling the "real" (Type *x; x->member; Type &y; y.member)
>>or "formal" (Type x; x.member) (type-associated) function still belongs
>>to the consuming programmer.
>
>And the consuming programmer can always say "d.Base::member()", too,
>overriding both static and dynamic typing.  But I confess that I the
>relevance of this language feature to a supposed conflict between
>overloading and virtual functions escapes me.

There is no "conflict" - they are complementary mechanisms.  However, 
getting them to work together to provide consistent behavior can be very
clumsy.  Especially when the compilers obey different sets of rules...

>>>I would contend that, once virtual functions have been introduced into
>>>a class hierarchy, the additional use of overloaded functions is the
>>>design error to be corrected, not some presumed deficiency in C++.
>>
>>So you are saying *never* to overload virtuals ?  If so, then that is the
>>strongest admission I can think of, that the present rules for doing so
>>are useless.
>
>No, no more than I would say *never* to use a goto.  What I *am*
>saying is that the use of an overloaded virtual function likely points
>out an opportunity to make a class design more regular.
>
>In the absence of other considerations, I would suggest that the
>overloading of the virtual function be subsumed into an overloaded
>constructor (or non-member function), and that the constructed object
>(or function return value) be used as an actual parameter of the
>no-longer-overloaded virtual function.

In other words, conversion ?  Now how can you claim that A.overloads/B.virtuals
/C.conversion have nothing to do with each other when you have transformed code
using A,B into code using B,C without affecting functionality at all ?

You are displaying clearly that they are all ways of accomplishing the *same*
thing, which is my point exactly.

>The advantage of this change is that the expansion of the overloaded
>alternatives requires only one change: an additional constructor or
>non-member function.  An overloaded virtual function, to support an
>equivalent expansion, would require N additional functions for the N
>classes that implement the given virtual function.

If the argument types should be convertible in all circumstances.  If it
is context-dependent, then you are stuck back with overloading.  If all
I am doing is printing out the date, I MAY NOT WANT the datestring char* to
be converted to a Date so I can print it out again - talk about overhead!

In this case I may well want to overload and accept char* as well as Date,
without invoking unnecessary conversions.

You are arguing FOR a form of "control" (exact type matching) that only
affects your own source code, but AGAINST a form of "control" (convertible
type matching) that can drastically improve runtime efficiency.

>>>The differences between these techniques is a tool, not a flaw.
>>It is a flaw, not a tool.
>
>We must agree to disagree, then.

The "difference of techniques" you cite is not a difference of techniques
at all but rather a difference of syntax only, and a set of C++ rules that 
absolutely forbids one set of techniques (tailored specification) from being 
used, despite its efficiency advantages.

Keep your "seam", if you must, but be aware that, given a call to a derived
object via a base pointer, C++ prefers to match derived-type functions with
all rules at its disposal FIRST, before trying to match base-type functions
at all.  So you have only half a cake, unless you are willing to invoke
these functions yourself directly.  If you are using g++, you may not be
aware of this - cfront does it correctly.

It is only half a cake for me, too, but it isn't the "paradigm shift" you
seem to describe it as.  C++ prefers classwise over argumentwise resolution,
and hopefully it is only a matter of time before this approach is extended
to all forms of type-safe signature tailoring in derived classes.

-- 
  Craig Hubley   "...get rid of a man as soon as he thinks himself an expert."
  Craig Hubley & Associates------------------------------------Henry Ford Sr.
  craig@gpu.utcs.Utoronto.CA   UUNET!utai!utgpu!craig   craig@utorgpu.BITNET
  craig@gpu.utcs.toronto.EDU   {allegra,bnr-vpa,decvax}!utcsri!utgpu!craig
