Topic: Overloading vs. virtual functions


Author: craig@gpu.utcs.utoronto.ca (Craig Hubley)
Date: 3 Mar 91 00:39:37 GMT
Raw View
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




Author: chip@tct.uucp (Chip Salzenberg)
Date: 4 Mar 91 18:56:45 GMT
Raw View
According to craig@gpu.utcs.utoronto.ca (Craig Hubley):
>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.

But, you see, the consumer needs to know!  The consumer is a C++
programmer who is presumably conversant with "Base& b = d" and other
type-related features.  That programmer wants and needs to know which
member functions are dependent on the dynamic type of an object and
which are not -- i.e. which are virtual and which aren't.

>Having the decision which function to call made at compile-time or
>run-time changes nothing.

It matters a great deal if the dynamic and static types disagree.

>>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 ...

I've been persuaded that the ClassID feature would have little impact
on efficiency.

>>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.
>
>Getting [static and dynamic typing] to work together to provide consistent
>behavior can be very clumsy.  Especially when the compilers obey different
>sets of rules...

I don't think they _can_ provide consistent behavior.  If two
mechanisms have distinct existence purely because of their
differences, then making them alike make the distinction between them
redundant.  Consider the application of this rule to dynamic and
static typing, i.e. virtual and non-virtual functions.

>>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 ?

Not necessarily.  I refer to explicit constructor calls that may have
more than one argument.  For example, let us consider class C
constructed with an overloaded virtual function a la Hubley:

    class C {
        virtual void larry(int, int);
        virtual void larry(long, long);
    };

In the Salzenberg variation, this becomes:

    class L {   // abstract superclass
        virtual void larry() = 0;
    };

    class INT_L : public L {
        INT_L(int init_i, int init_j)   { i = init_i; j = init_j; }
        virtual void larry();
    private:
        int i, j;
    };

    class C {
        virtual void larry(const L&);
    };

In this scheme, INT_L handles what used to be "larry(int,int)".
Expansion of the the larry() feature to new argument list involves
creation of one new class derived from L, one or more constructors for
that class, and one new version of L::larry().  Surely this is a
better choice than introducing yet another overloaded larry() function
which then must be implemented in all subclasses of C.  (Unless, of
course, the inheritance tree rooted at C is very shallow.)

>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?

The fact that an algorithm that uses an old feature set can be recast
using a new feature set does not imply that the new feature set is in
all ways equivalent to the old.

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

Yes, well, so is assembler, if you define "same thing" broadly enough.

>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!

Using the above example, the L class not cause any conversions
whatsoever until they are needed.  The concept is rather like those
tiny classes returned by typical "operator[]" functions: they don't
actually _do_ anything until assigned to or accessed or whatever.

>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.

If you would be so kind as to give a reasonably complete definition of
"convertible type matching," I would be obliged.  (Unless it refers to
the freedom to modify virtual function definitions in ways that can be
adapted for using automatic type conversions; I understand that, and I
consider it unimportant.)

>The "difference of techniques" you cite is not a difference of techniques
>at all but rather a difference of syntax only ...

Surely you don't mean that virtual functions and non-virtual functions
differ only in syntax.  What _do_ you mean?

>C++ prefers to match derived-type functions with all rules at its disposal
>FIRST, before trying to match base-type functions at all.

Of course.




Author: chip@tct.uucp (Chip Salzenberg)
Date: 28 Feb 91 14:29:17 GMT
Raw View
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.

>You can remove the distinction [between compile-time and run-time binding]
>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.

>[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.

>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.

>>>When you mix virtuals and overloading, things get scary ...
>>"Well, don't do that, then."
>Then C++ is a far less powerful language.

I fail to see the weakness implied by this statement.

>>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.

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.

>>The differences between these techniques is a tool, not a flaw.
>It is a flaw, not a tool.

We must agree to disagree, then.
--
Chip Salzenberg at Teltronics/TCT      <chip@tct.uucp>, <uunet!pdn!tct!chip>
"It's not a security hole, it's a SECURITY ABYSS." -- Christoph Splittgerber
   (with reference to the upage bug in Interactive UNIX and Everex ESIX)