[comp.std.c++] overloading vs. virtual functions

chip@tct.uucp (Chip Salzenberg) (02/21/91)

According to craig@gpu.utcs.utoronto.ca (Craig Hubley):
>The fact that C++ implements the two mechanisms [virtual functions and
>overloaded functions] so as to create (in my opinion) counter-intuitive
>effects is (in my opinion) a flaw in C++.  That is, the programmer must
>be very much aware of the "seam" between compile-time and run-time ...

This fact is a *necessary* consequence of one of C++'s design goals,
namely, that a programmer never pay more in efficiency than necessary.
That means that it is the programmer who decides when to use run-time
binding (virtual) and when to use compile-time binding (overloading).
If you remove the distinction, you remove the control -- a tradeoff
that is, to me, unacceptable.

>Consider how static initialization works in C++.  The initialization
>syntax means that some functions consist entirely of initialization
>and calls to these can (in principle) be resolved entirely at compile-
>time if they are called with static arguments.

Not so!  Those initializations cannot, even in principle, be resolved
at compile time if the actual types of their reference or pointer
parameters are unknown at compile time -- which is exactly the
situation for which virtual functions were created.  So your scenario
is only true in a "(C++)--" language without virtual functions.
That's one way to merge two features: get rid of one of them.  :-)

>>a base class B and a derived class D:
>>
>>    extern void foo(B&);
>>    extern void foo(D&);
>>    D d; B& b = d; foo(b);
>>
>>It is the "foo(B&)" function that will be called ...
>>
>>    D d; B& b = d; b.foo();
>>
>>It is B::foo() that is called ...
>
>In neither of the cases above can the system be expected to read the
>programmer's mind and "decide" to call the function associated with
>the "real" rather than the "formal" type.  In fact, C++ explicitly uses
>the -> vs. . notation so that the programmer can make that decision
>him/herself.

The notation is irrelevant.  Given a pointer "a" with member "b",
"a->b" and "(*a).b" are exactly equivalent in all cases.  I suppose
you knew that; so what did you really mean?

>There is no "cast to the dynamic type" operator in C++ that would
>delay this decision until run-time.  You can't even find out what
>the type is.

That's a necessary result of the most basic type rules.  And there's
nothing special about overloaded functions in this context: normal
functions and non-virtual member functions are in exactly the same
boat.  If dynamic type loss is unendurable to you, then you're using
the wrong language, because it's not going away.

>You have a good analogy there, but what you are proving is that overloading
>is solving the same problem as member functions, but without the other half
>of its brain:  runtime resolution.  When you mix virtuals and overloading,
>things get scary ...

"Well, don't do that, then."

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++.
The differences between these techniques is a tool, not a flaw.

As a practical implementation issue, it is obvious that non-friend
non-member functions are indefinite in number.  Presuming a vanilla
implementation of virtual functions, how can a newly created object
have a virtual function table containing the addresses of overloaded
functions that haven't even been compiled yet?

>But if "orthogonal" in this sense is supposed to mean "never interact
>at all" you are wrong.

But that's not what I meant, because that's not what the word means!
Compare the Jargon File 2.6.3:

orthogonal: [from mathematics] adj. Mutually independent;
   well separated; sometimes, irrelevant to.  Used in a generalization
   of its mathematical meaning to describe sets of primitives or
   capabilities which, like a vector basis in geometry, span the
   entire `capability space' of the system and are in some sense
   non-overlapping or mutually independent.  ...

A language may have templates or not, and it may have inheritance or
not; so the features are orgthogonal.  But of course they interact;
it's all one language.

>As a programmer, I couldn't care less at what point in the
>compile/link/load/run process these things are resolved, except
>where the language forces me to be aware of it.

Well, then, you're using the wrong language, or else you need to
change your thinking.  (No smiley here.)  If your mental view of the
type system does not include the distinction between compile time and
run time, then C++ is simply a bad match for the way you think.  As I
mentioned up front, the *design goals* of C++ simply cannot allow all
type resolution to be (conceptually) delayed until run time.  Such a
language wouldn't be C++ any more.

>Obviously you have to be aware of these subtle differences sometimes but
>inventing wholly different syntax for each situation is IMHO a mistake.

Virtual and non-virtual member functions use the same syntax; the only
difference is one keyword.  However, once you decide: "But I don't
want to use a member function," then you have stepped outside the
bounds of "same things," so you shouldn't expect "same syntax."
-- 
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)

dsouza@optima.cad.mcc.com (Desmond Dsouza) (02/25/91)

In article <27C2D4B8.3AD3@tct.uucp> chip@tct.uucp (Chip Salzenberg) writes:


	>   >But if "orthogonal" in this sense is supposed to mean "never interact
	>   >at all" you are wrong.
	>
	>   But that's not what I meant, because that's not what the word means!
	>   Compare the Jargon File 2.6.3:
	>
	>   orthogonal: [from mathematics] adj. Mutually independent;
	>      well separated; sometimes, irrelevant to.  Used in a generalization
	>      of its mathematical meaning to describe sets of primitives or
	>      capabilities which, like a vector basis in geometry, span the
	>      entire `capability space' of the system and are in some sense
	>      non-overlapping or mutually independent.  ...
	>
	>   A language may have templates or not, and it may have inheritance or
	>   not; so the features are orgthogonal.  But of course they interact;
	>   it's all one language.

In discussing programming language features, "orthogonal" has a more
specific meaning. Here is one (from M. Jazayeri, "Programming Language
Concepts", p.15)  :

	The principle of 'orthogonality': language features can be
        composed in a free and uniform manner with predictable effects
	and without limitations.

i.e. any meaningful composition of language constructs should be allowed. 

For example, here are some reasons why Pascal is not orthogonal:
1. Files cannot be passed by value.
2. Functions can only return values of some restricted types.

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

Desmond.
--

-------------------------------------------------------------------------------
 Desmond D'Souza, MCC CAD Program | ARPA: dsouza@mcc.com | Phone: [512] 338-3324
 Box 200195, Austin, TX 78720 | UUCP: {uunet,harvard,gatech,pyramid}!cs.utexas.edu!milano!cadillac!dsouza

craig@gpu.utcs.utoronto.ca (Craig Hubley) (02/26/91)

In article <27C2D4B8.3AD3@tct.uucp> chip@tct.uucp (Chip Salzenberg) writes:
>According to craig@gpu.utcs.utoronto.ca (Craig Hubley):
>>The fact that C++ implements the two mechanisms [virtual functions and
>>overloaded functions] so as to create (in my opinion) counter-intuitive
>>effects is (in my opinion) a flaw in C++.  That is, the programmer must
>>be very much aware of the "seam" between compile-time and run-time ...
>
>This fact is a *necessary* consequence of one of C++'s design goals,
>namely, that a programmer never pay more in efficiency than necessary.

Sometimes you must/can be aware of the seam.  However, it is possible for
the differences to be minimized and made consistent.  Again, nobody is
suggesting that a programmer or library who does not use a mechanism
should pay the efficiency price for that mechanism.  No such compromise
is required, and I think would be unacceptable in C++.

>That means that it is the programmer who decides when to use run-time
>binding (virtual) and when to use compile-time binding (overloading).
>If you remove the distinction, you remove the control -- a tradeoff
>that is, to me, unacceptable.

You can remove the distinction 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) the user needn't be concerned with the fact that I have
actually defined two separate functions and one or the other is called, so
long as I have done my homework, made my functions consistent, and documented
what to expect in each case.  I still have the "control" of allowing Date to
take another argument list, or making its functions virtual, etc..  I have
simply removed a detail from the user's sight, UNLESS he/she chooses to
investigate it, in which case, as you state, the fact that two functions
(that may be totally independent of each other) exist.

In other words, I could build one function Date(year, month, day) and provide
a variety of interfaces that do nothing but find/rearrange the arguments and
call it, or I could build several independent functions.  All of these choices
have runtime efficiency implications (e.g. Date(year, month, day) will be
guaranteed faster than Date(char*)) but this is not explicit in the syntax
the programmer uses.  It is an implementation concern.

>>Consider how static initialization works in C++.  The initialization
>>syntax means that some functions consist entirely of initialization
>>and calls to these can (in principle) be resolved entirely at compile-
>>time if they are called with static arguments.
>                         ^^^^^^^^^^^^^^^^^^^^^
>Not so!  Those initializations cannot, even in principle, be resolved
>at compile time if the actual types of their reference or pointer
>parameters are unknown at compile time -- which is exactly the
>situation for which virtual functions were created.  So your scenario

Then they wait until runtime.  But the types of many values *are* known
at compile time.  I guess I should rephrase the underlined portion as
"with static arguments that have no (derived types AND virtual functions)".
You are right, in these cases the functions to be called are ambiguous
and no such compile-time resolution could occur.

>>programmer's mind and "decide" to call the function associated with
>>the "real" rather than the "formal" type.  In fact, C++ explicitly uses
>>the -> vs. . notation so that the programmer can make that decision
>>him/herself.
>
>The notation is irrelevant.  Given a pointer "a" with member "b",
>"a->b" and "(*a).b" are exactly equivalent in all cases.  I suppose
>you knew that; so what did you really mean?

Yes, sorry.  The notation is irrelevant.  However, 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.
Regardless of the version of the "real" notation used,  he/she still has
the option of calling the "formal" version.  In fact, this is still the
default way to access members (no * or & or -> required!).  So this choice
is STILL up to the consuming programmer, even though the producing
programmer can decide to provide a more dynamically resolved function.

Therefore, it is false that providing a more powerful and subtle mechanism
to the producing programmer must necessarily impose either an inefficiency
or a clumsy syntax on the consuming programmer.  Unless you consider ->
clumsy.

>>There is no "cast to the dynamic type" operator in C++ that would
>>delay this decision until run-time.  You can't even find out what
>>the type is.
>
>That's a necessary result of the most basic type rules.  And there's

Then why are Bjarne/Andy in favor of it ?  At least so I've heard...
although apparently they want failures to raise an exception.

Again, do not believe that a language that supports C's original types
and conversions, plus its own, plus the base/derived conversions, has
ANY "most basic type rules".  If anything, it has several conflicting
sets of rules and an oft-quoted "double standard" between builtin and
user-defined types.

>nothing special about overloaded functions in this context: normal
>functions and non-virtual member functions are in exactly the same
>boat.  If dynamic type loss is unendurable to you, then you're using
>the wrong language, because it's not going away.

Dynamic type loss is OK if the type is not useful at runtime.  However,
it can be useful in many more ways than deciding which version of its
predefined (and often unchangeable) functions to call at runtime.  These 
other uses can be supported without requiring every object to carry a
type tag through to runtime.

>>You have a good analogy there, but what you are proving is that overloading
>>is solving the same problem as member functions, but without the other half
>>of its brain:  runtime resolution.  When you mix virtuals and overloading,
>>things get scary ...
>
>"Well, don't do that, then."

Then C++ is a far less powerful language.  Others have already pointed
out some of the problems created by the counter-intuitive/restrictive rules 
for virtuals/overloads.  I have already pointed out that every other "O-O"
language supports the necessary changes.  There are two ways for C++ to
become another Ada (i.e. a failure):
	- try to incorporate *every* way of doing something
	- fail to incorporate *at least one* way of doing something

As it stands, you cannot add any type- and context-specific functionality 
to an object without access to its source code.  This greatly restricts
reusability, to a level between that of say, Eiffel, and that of Ada.

I do not consider this sufficient to support a components industry, as it
leaves too many ways for producing programmers to cut off options for those
building on their code.  Therefore, most consuming programmers won't build
on others' code.  In your applications, this may be fine.  In mine, they
aren't.  There are other issues too, like overall source code size and
a minimal number/namespace of functions.

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

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

It is a flaw, not a tool.

>As a practical implementation issue, it is obvious that non-friend
>non-member functions are indefinite in number.  Presuming a vanilla
>implementation of virtual functions, how can a newly created object
>have a virtual function table containing the addresses of overloaded
>functions that haven't even been compiled yet?

They can't.  Which is why the type tag is necessary to sort out type-
dependent processing.  You are continuing to prove that defining a new
virtual in the base type, or allowing it to be "patched in" is very
bad solution.  What do you mean by "vanilla implementation"?

>orthogonal: [from mathematics] adj. Mutually independent;
>   well separated; sometimes, irrelevant to.  Used in a generalization
>   of its mathematical meaning to describe sets of primitives or
>   capabilities which, like a vector basis in geometry, span the
>   entire `capability space' of the system and are in some sense
>   non-overlapping or mutually independent.  ...
>
>A language may have templates or not, and it may have inheritance or
>not; so the features are orgthogonal.  But of course they interact;
>it's all one language.

Then they are mutually independent but not necessarily "well separated"
(they both impact the same source code) and certainly not "irrelevant to".
I would suggest it is still a poor choice of word.  It emphasizes the
PoV of a mediocre compiler designer who can't figure out how to deal
with their interaction, rather than a programmer using the compiler who
must write source code that works predictably using both mechanisms.

Latitude and longitude are "orthogonal" in this sense too, as no doubt
observed by the Captain of the Exxon Valdez... who was probably staring
at the pretty lights on his console as his ship went up on the rocks.

>>As a programmer, I couldn't care less at what point in the
>>compile/link/load/run process these things are resolved, except
>>where the language forces me to be aware of it.
>
>Well, then, you're using the wrong language, or else you need to
>change your thinking.  (No smiley here.)  If your mental view of the

I reject both of your options.  I don't mind if the language forces me
to be aware of the distinction where necessary for efficiency.  But
th distinction we are discussing is "*not* necessary for efficiency but 
something falsely called "strong typing".

>type system does not include the distinction between compile time and
>run time, then C++ is simply a bad match for the way you think.  As I
>mentioned up front, the *design goals* of C++ simply cannot allow all
>type resolution to be (conceptually) delayed until run time.  Such a
>language wouldn't be C++ any more.

I agree with the design goals.  However, conceptually/potentially delaying 
type resolution until runtime for all objects (i.e. supporting a typeof(x) or
x.has_member(diamter), etc.) is being implemented in some C++ compilers
and is under consideration by ANSI in a variety of forms.

The authors of those compilers (and ANSI!) would be surprised to learn that 
	- C++ programmers don't want it
	- C++ don't need it
	- They are no longer writing C++ compilers.

>>Obviously you have to be aware of these subtle differences sometimes but
>>inventing wholly different syntax for each situation is IMHO a mistake.
>
>Virtual and non-virtual member functions use the same syntax; the only
>difference is one keyword.  However, once you decide: "But I don't
>want to use a member function," then you have stepped outside the
>bounds of "same things," so you shouldn't expect "same syntax."

Wrong.  Once I decide "I don't want an attribute that is associated with
the object" then I have stepped outside the bounds of "same things".  An
object is clearly associated (very strongly) with its type.


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

chip@tct.uucp (Chip Salzenberg) (02/28/91)

According to dsouza@optima.cad.mcc.com (Desmond Dsouza):
>In discussing programming language features, "orthogonal" has a more
>specific meaning. Here is one (from M. Jazayeri, "Programming Language
>Concepts", p.15)  :
>
>	The principle of 'orthogonality': language features can be
>        composed in a free and uniform manner with predictable effects
>	and without limitations.
>
>i.e. any meaningful composition of language constructs should be allowed.

I cannot accept this definition.  It is too broad for useful dialogue.
Unless, of course, the statement ``No currently existing computer
language is orthogonal'' is useful dialogue.  :-)
-- 
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)

chip@tct.uucp (Chip Salzenberg) (02/28/91)

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)

craig@gpu.utcs.utoronto.ca (Craig Hubley) (03/03/91)

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

chip@tct.uucp (Chip Salzenberg) (03/05/91)

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.

djones@megatest.UUCP (Dave Jones) (03/05/91)

From article <1991Feb25.201923.14554@gpu.utcs.utoronto.ca), by craig@gpu.utcs.utoronto.ca (Craig Hubley):
) In article <27C2D4B8.3AD3@tct.uucp) chip@tct.uucp (Chip Salzenberg) writes:
))According to craig@gpu.utcs.utoronto.ca (Craig Hubley):
)))The fact that C++ implements the two mechanisms [virtual functions and
)))overloaded functions] so as to create (in my opinion) counter-intuitive
)))effects is (in my opinion) a flaw in C++.  That is, the programmer must
)))be very much aware of the "seam" between compile-time and run-time ...

Anybody who can not handle this "seam" should be programming in LOGO.

craig@gpu.utcs.utoronto.ca (Craig Hubley) (03/08/91)

In article <15488@prometheus.megatest.UUCP> djones@megatest.UUCP (Dave Jones) writes:
>From article <1991Feb25.201923.14554@gpu.utcs.utoronto.ca), by craig@gpu.utcs.utoronto.ca (Craig Hubley):
>)))The fact that C++ implements the two mechanisms [virtual functions and
>)))overloaded functions] so as to create (in my opinion) counter-intuitive
>)))effects is (in my opinion) a flaw in C++.  That is, the programmer must
>)))be very much aware of the "seam" between compile-time and run-time ...
>
>Anybody who can not handle this "seam" should be programming in LOGO.

Yeah, and anyone who can't handle raw bit-shifting should use Pascal.
While we're at it, who needs databases ?  I can open files and dig through
'em.  And who needs CASE ?  I got a gaw-damm disassemblah heah, boah!

I can "handle it".  I have programmed in LOGO, and LISP, and FORTH, and
there are definitely advantages to languages that have interpreters AND
compilers that can make the code act the same.  I have also programmed
in 6502 assembler, 6809 assembler, 68000 assembler, WSL, weird BASICs, C,
and other languages that gave access to the raw bit-level of the box.
Portable code doing this kind of manipulation is far harder to get right,
because the fact that it works on the first 10 boxes you tried it on doesn't
tell you much about the 11th.

Accordingly, modern programming languages don't force you to do much
bit-shifting, allowing you to rely on higher-level primitives most of the
time and only descend into bits when you really need to do so.  The same
is true of compile-time vs. run-time binding, as others have already quite
adequately demonstrated by comparison to other languages.

Nobody is saying it can always be hidden completely, particularly in real-
time applications.  But most of us are saying that it isn't necessary for
language syntax to force programmers to hard-code the difference into
every scrap of code they write.  Consider, what if you write a piece of
nice general source code to deal with three different kinds of objects -
and then call it in a context where, by the language definition, the
type of that argument is KNOWN, unambiguously.

I can't ask my compiler to take advantage of this potential optimization, 
if I have also asked it to guarantee me the difference between runtime 
and compile-time binding.  To deal with this situation in C++ as it stands, 
I have to write three different functions and call THOSE when I know what
the type is.  I can never let the compiler figure it out for me.  If I
want it to do so, I gotta suffer the virtual lookup overhead every time.

The much-prized machine-level efficiency you bit-bangers are so enamored of
comes much more cheaply when it can be generated by a compiler and not a 
human programmer.  In the dark ages of C, if you wanted to avoid unnecessary
function call overhead you had to rewrite your function as a macro.  Often
you had a function version and a macro version side-by-side for use in
different circumstances.  In the more enlightened C++, we use "inline" and
let the compiler do it.  Of course we are AWARE of the difference, but it
doesn't cause us anywhere near the inconvenience, doesn't make us maintain
two versions, and if "inline" doesn't always guarantee savings, then that's 
all right.  We have to profile the code anyway.

Of course, there is always the possibility that you will not understand
the rules that the compiler uses to make these decisions.  If the only
way a programmer can tell what his code will do, is to hard-code all of
its runtime behavior himself, then I suggest you go back to assembler, 
where every little choice was up to you.  And where you had to hack up
your code to make the tiniest little optimization.


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