View mode: basic / threaded / horizontal-split · Log in · Help
August 07, 2007
Proposition for change in D regarding Inheriting overloaded methods
OK,

So I originally posted the Overloading/Inheritance question as I was 
confused by the behavior of the current D compiler.  After reading all of 
the responses arguments, I believe there are two main camps in the debate. 
One camp, which I'll name the C++ camp, believes the C++ behavior is the 
best approach.  This camp includes Walter, and believes the current 
implementation of the D compiler (which mimics C++) is best.  The second 
camp includes myself, and several other users of D who were either aware of 
this issue, or unaware and surprised by the current implementation.  I'll 
call this the Java camp, as the behavior I desire is most closely imitating 
Java (although not exactly).  There may be other ways of solving this 
problem, and I welcome those to voice their opinions.  I will try to respond 
to everyone.

Now, I am by no means an expert in anything to do with writing languages, so 
this proposal may come across as a bit stumbly or informal, but I think it's 
important that I start a new thread in which to sort of draw attention to 
the fact that I am no longer asking questions about the current 
implementation, nor am I submitting a bug.  What I believe is that the 
specification itself should be changed.  So I'll make my proposal, and let 
everyone attempt to shoot holes in it/bolster it, until hopefully there is a 
decision by those important enough to make the changes on whether a change 
should be made.

For those of you who were confused by the original question, I'll describe 
the behavior of D as it stands, and why I have issues with it.

When a class inherits from another class, and redefines one of the base 
class' methods, using the same name and parameter types, the derived class 
overrides that method.  However, if the method is overloaded, the base 
class' methods are not considered when calling that method.  This is the 
case even when the derived class has no suitable overrides for the call in 
question.  For example (augmented example from spec):

class A
{
  int foo(int x) { ... }
  int foo(long y) { ... }
  int foo(char[] s) { ... }
}

class B : A
{
 override int foo(long x) { ... }
}

void test()
{
 B b = new B();
 A a = b;
 b.foo(1);   // calls B.foo(long), since A.foo(int) not considered
 a.foo(1);   // calls A.foo(int) because it is not overrided by B
 b.foo("hello");     // generates a compiler error because A is not 
considered for overrides
 a.foo("hello");     // calls A.foo(char[])
}

To have the compiler consider the base class' overloads before considering 
the derived class' overloads, an alias can be added:

class B : A
{
 alias A.foo foo;
 override int foo(long x) { ... }
}

void test()
{
 B b = new B();
 A a = b;
 b.foo(1);   // calls A.foo(int)
 a.foo(1);   // calls A.foo(int)
 b.foo("hello");     // calls A.foo(char[])
 a.foo("hello");     // calls A.foo(char[])
}

Thus ends the definition of the issue.  Here is where my opinion comes in. 
There are two issues in this scenario, and both of them have to do with the 
intentions of the author of class B.  I think everyone agrees that the 
author of B intended to handle the case where foo(long) is called.

However, does the author intend to handle the case where foo(int) is called? 
Let's assume he does (which is what the current compiler assumes).  The 
author has not forbidden the user of the class from calling A.foo, because 
the user can simply cast to an A object, and call A.foo(int) directly. 
Therefore, if the author meant to override foo(int) by defining just a 
foo(long), he has failed.  From these points, I believe that the above code 
is an example of an incorrectly implemented override, and I believe that the 
correct response the compiler should have is to error out, not while 
compiling test(), but while compiling B, indicating to the user that he 
should either declare the alias, or override foo(int).  This is the point in 
which my solution differs from Java.  Java would allow this to proceed, and 
call the base class' foo(int) in all cases, which is not what the author 
intended.

The second issue is how to handle the foo(char[]) case.  In the current 
implementation, because A is not searched for overrides, the compiler 
produces an error indicating that the user tried to call the foo(long) 
method, but there is no implicit conversion from char[] to long.  There are 
two possibilities.  One is that the author did not notice that A.foo(char[]) 
existed, and intended to override all instances of foo() with his foo(long) 
override.  However, the author is NOT notified of this issue, only the user 
of the class is notified of this issue.  So the potential for unambiguous 
code to have escaped exists.  The second possibility is that the author 
fully intended to allow the base class to define foo(char[]), but forgot to 
define the alias.  Again, since the compiler gives no error, he is unaware 
that he is releasing buggy code to the world.  I believe the correct 
assumption of the compiler should be that the user wanted the alias for the 
base class' foo(char[]), and should alias it implicitly if and only if no 
suitable match exists on the derived class.  In the case where the author 
did not notice foo(char[]) existed, he problably doesn't mind that 
foo(char[]) is defined by the base class.  If a suitable match exists that 
is not a direct override of the base class, then the issue reduces to the 
previous case, where an implicit conversion is required, and the compiler 
should error out.

There is one other possibile solution that I would be willing to concede to, 
and that is that the compiler errors out if the base class does not override 
all overloads of a particular method name.  This forces the user to either 
override all overloads of the method, or define the alias.  This would be 
the safest solution, as the author of B must make his intentions perfectly 
clear.

So my proposal is to change the specification so that:

If there is a class A, which is a base class of class B, where A defines a 
method foo(args), and B defines a method foo(args2), such that the types of 
args2 cannot be implicitly converted to args, and B does not define 
foo(args), then the definition of B.foo(args) shall be implicitly aliased to 
A.foo(args).  If, using the same assumptions, args2 can be implicitly 
converted to args, then the compiler should fail to compile B indicating 
that the user must define B.foo(args) by override or by alias.

I believe this will give us the best of both camps, and allow much less code 
to be released with silent bugs than the current implementation.

Let the hole shooting begin...

-Steve
August 07, 2007
Re: Proposition for change in D regarding Inheriting overloaded methods
Steven Schveighoffer wrote:
> However, does the author intend to handle the case where foo(int) is called? 
> Let's assume he does (which is what the current compiler assumes).  The 
> author has not forbidden the user of the class from calling A.foo, because 
> the user can simply cast to an A object, and call A.foo(int) directly. 
> Therefore, if the author meant to override foo(int) by defining just a 
> foo(long), he has failed.  From these points, I believe that the above code 
> is an example of an incorrectly implemented override, and I believe that the 
> correct response the compiler should have is to error out, not while 
> compiling test(), but while compiling B, indicating to the user that he 
> should either declare the alias, or override foo(int).  This is the point in 
> which my solution differs from Java.  Java would allow this to proceed, and 
> call the base class' foo(int) in all cases, which is not what the author 
> intended.

The next update of the compiler will throw a runtime exception for this 
case.

> The second issue is how to handle the foo(char[]) case.  In the current 
> implementation, because A is not searched for overrides, the compiler 
> produces an error indicating that the user tried to call the foo(long) 
> method, but there is no implicit conversion from char[] to long.  There are 
> two possibilities.  One is that the author did not notice that A.foo(char[]) 
> existed, and intended to override all instances of foo() with his foo(long) 
> override.  However, the author is NOT notified of this issue, only the user 
> of the class is notified of this issue.  So the potential for unambiguous 
> code to have escaped exists.

But a compile time error is still generated, so I don't regard this as a 
big problem. The big problems are silent hijacking of code.


> The second possibility is that the author 
> fully intended to allow the base class to define foo(char[]), but forgot to 
> define the alias.  Again, since the compiler gives no error, he is unaware 
> that he is releasing buggy code to the world.  I believe the correct 
> assumption of the compiler should be that the user wanted the alias for the 
> base class' foo(char[]), and should alias it implicitly if and only if no 
> suitable match exists on the derived class.  In the case where the author 
> did not notice foo(char[]) existed, he problably doesn't mind that 
> foo(char[]) is defined by the base class.

The problem with code that looks like a mistake, but the compiler makes 
some assumption about it and compiles it anyway, is that the code 
auditor cannot tell if it was intended behavior or a coding error. 
Here's a simple example:

void foo()
{ int i;
  ...
  { int i;
    ...
    use i for something;
  }
}

To a code auditor, that shadowing declaration of i looks like a mistake, 
because possibly the "use i for something" code was meant to refer to 
the outer i, not the inner one. (This can happen when code gets updated 
by multiple people.) To determine if it was an actual mistake, the code 
auditor is in for some serious spelunking. This is why, in D, shadowing 
declarations are illegal. It makes life easier for the auditor, because 
code that looks like a mistake is not allowed.

> If a suitable match exists that 
> is not a direct override of the base class, then the issue reduces to the 
> previous case, where an implicit conversion is required, and the compiler 
> should error out.
> 
> There is one other possibile solution that I would be willing to concede to, 
> and that is that the compiler errors out if the base class does not override 
> all overloads of a particular method name.  This forces the user to either 
> override all overloads of the method, or define the alias.  This would be 
> the safest solution, as the author of B must make his intentions perfectly 
> clear.

I am not comfortable with this method, as it will force the derived 
class programmer to implement overloads that may not be at all meant to 
exist in the API he defines for that class. I think he should be in full 
control of the API for the class, and not forced to provide 
implementations of functions that may be irrelevant clutter. I prefer 
the solution where attempts to call unoverridden base class overloads 
will result in a runtime exception.

> I believe this will give us the best of both camps, and allow much less code 
> to be released with silent bugs than the current implementation.

I believe that the enforcing of the override attribute, and the runtime 
exception case as described, closes all the known hijacking issues.
August 07, 2007
Re: Proposition for change in D regarding Inheriting overloaded methods
Steven Schveighoffer wrote:
> So my proposal is to change the specification so that:
> 
> If there is a class A, which is a base class of class B, where A defines a 
> method foo(args), and B defines a method foo(args2), such that the types of 
> args2 cannot be implicitly converted to args, and B does not define 
> foo(args), then the definition of B.foo(args) shall be implicitly aliased to 
> A.foo(args).  If, using the same assumptions, args2 can be implicitly 
> converted to args, then the compiler should fail to compile B indicating 
> that the user must define B.foo(args) by override or by alias.
> 
> I believe this will give us the best of both camps, and allow much less code 
> to be released with silent bugs than the current implementation.
> 
> Let the hole shooting begin...

I like it.  In fact it solves the existing problem exhibited by the 
original example 2!

Here are our original examples given by Walter and used to support the 
current C++ like behaviour.

-----------------------------------
class X1 { void f(int); }

// chain of derivations X(n) : X(n-1)

class X9: X8 { void f(double); }

void g(X9 p)
{
    p.f(1);    // X1.f or X9.f ?
}
-----------------------------------
class B
{    long x;
     void set(long i) { x = i; }
    void set(int i) { x = i; }
    long squareIt() { return x * x; }
}
class D : B
{
    long square;
    void set(long i) { B.set(i); square = x * x; }
    long squareIt() { return square; }
}
long foo(B b)
{
    b.set(3);
    return b.squareIt();
}
-----------------------------------

Under your proposal these would both be classify as "incorrectly 
implemented override"'s and fail to compile with an error.

In example 1 the error would occur when compiling X9.  In example 2 the 
error would occur when compiling D.  In both cases the addition of 
'alias' or an exact override for the 'int' method from the base will 
resolve the error.

In example 1, if other overloads existed in X2 thru X8 i.e. "void 
f(short);" then both "void f(int);" and "void f(short);" would need to 
be aliased or defined in X9 to resolve the error.  This seems to 
indicate that all base classes all the way back to the root need to be 
examined, that could be complex...

As I mentioned earlier your proposal will solve the existing problem 
caused by example 2, which under both C++ and Java like implementations 
calls B.set(int) then D.squareit() resulting in 0 instead of 9.

That, combined with the implicit alias of overloads where no implicit 
conversion is possible and I think it will be a feature which is both 
safe and intuitive.

I'm interested to see what other people think.

Regan
August 07, 2007
Re: Proposition for change in D regarding Inheriting overloaded methods
Walter Bright wrote:
> The next update of the compiler will throw a runtime exception for this 
> case.

So, in this case:

class B
{    long x;
     void set(long i) { x = i; }
    void set(int i) { x = i; }
    long squareIt() { return x * x; }
}
class D : B
{
    long square;
    void set(long i) { B.set(i); square = x * x; }
    long squareIt() { return square; }
}
long foo(B b)
{
    b.set(3);
    return b.squareIt();
}

when the call to b.set(3) is made you insert a runtime check which looks 
for methods called 'set' in <actual type of object>, if none of them 
take a <insert types of parameters> you throw an exception.

Is this done at runtime instead of compile time because the parameters 
cannot always be determined at compile time?

>> The second possibility is that the author fully intended to allow the 
>> base class to define foo(char[]), but forgot to define the alias.  
>> Again, since the compiler gives no error, he is unaware that he is 
>> releasing buggy code to the world.  I believe the correct assumption 
>> of the compiler should be that the user wanted the alias for the base 
>> class' foo(char[]), and should alias it implicitly if and only if no 
>> suitable match exists on the derived class.  In the case where the 
>> author did not notice foo(char[]) existed, he problably doesn't mind 
>> that foo(char[]) is defined by the base class.
> 
> The problem with code that looks like a mistake, but the compiler makes 
> some assumption about it and compiles it anyway, is that the code 
> auditor cannot tell if it was intended behavior or a coding error. 
> Here's a simple example:
> 
> void foo()
> { int i;
>   ...
>   { int i;
>     ...
>     use i for something;
>   }
> }
> 
> To a code auditor, that shadowing declaration of i looks like a mistake, 
> because possibly the "use i for something" code was meant to refer to 
> the outer i, not the inner one. (This can happen when code gets updated 
> by multiple people.) To determine if it was an actual mistake, the code 
> auditor is in for some serious spelunking. This is why, in D, shadowing 
> declarations are illegal. It makes life easier for the auditor, because 
> code that looks like a mistake is not allowed.

It took me a while (because the example seems to be about something 
totally different) but I think the argument you're making is that you 
would prefer an error, requiring the author to specify what they want 
explicitly, rather than for the compiler to make a potentially incorrect 
assumption, silently. Is that correct?

In the original example (trimmed slightly):

class A
{
   int foo(int x) { ... }
   int foo(long y) { ... }
   int foo(char[] s) { ... }
}

class B : A
{
  override int foo(long x) { ... }
}

void test()
{
  B b = new B();
  A a = b;

  b.foo("hello");     // generates a compiler error
  a.foo("hello");     // calls A.foo(char[])
}

you're already making an assumption, you're assuming the author of B 
does not want to expose foo(char[]) and it's the fact that this 
assumption is wrong that has caused this entire debate.

As others have mentioned, this assumption destroys the "is-a" 
relationship of inheritance because "foo(char[])" is a method of A but 
not a method of B.  Meaning B "isn't-a" A any more... unless you've 
referring to a B with a reference to an A, when suddenly, it is.

Crazy idea, could the compiler (when it fails to match this overload) 
cast the object to it's base class and try again, repeat until you hit 
Object.  I guess this would essentially be a modification of the method 
lookup rules ;)


Making the opposite assumption (implicitly aliasing the "foo(char[])") 
doesn't introduce any silent bugs (that I am aware of) and restores the 
"is-a" relationship.

If the author really didn't want to expose "foo(char[])" then why were 
they deriving their class from A?  It goes against the whole idea of 
inheritance, doesn't it?

In special cases perhaps this is valid, in which case the author should 
explicitly define an overload and throw and exception or assert or both.

Note that I said "special cases" above, I think the most common case is 
that the "foo(char[])" should be implicitly aliased into the derived class.

>> If a suitable match exists that is not a direct override of the base 
>> class, then the issue reduces to the previous case, where an implicit 
>> conversion is required, and the compiler should error out.
>>
>> There is one other possibile solution that I would be willing to 
>> concede to, and that is that the compiler errors out if the base class 
>> does not override all overloads of a particular method name.  This 
>> forces the user to either override all overloads of the method, or 
>> define the alias.  This would be the safest solution, as the author of 
>> B must make his intentions perfectly clear.
> 
> I am not comfortable with this method, as it will force the derived 
> class programmer to implement overloads that may not be at all meant to 
> exist in the API he defines for that class. I think he should be in full
> control of the API for the class, and not forced to provide 
> implementations of functions that may be irrelevant clutter. 

I agree, but I get the impression this was the least favoured 
suggestion, probably for the very reason you mention here.

>> I believe this will give us the best of both camps, and allow much 
>> less code to be released with silent bugs than the current 
>> implementation.
> 
> I believe that the enforcing of the override attribute, and the runtime 
> exception case as described, closes all the known hijacking issues.

You're probably correct, but it doesn't solve the "compiler makes the 
wrong assumption" problem or the "irritating behaviour" problem ;)

Regan
August 07, 2007
Re: Proposition for change in D regarding Inheriting overloaded methods
Walter Bright Wrote:

> Steven Schveighoffer wrote:
> > However, does the author intend to handle the case where foo(int) is called? 
> > Let's assume he does (which is what the current compiler assumes).  The 
> > author has not forbidden the user of the class from calling A.foo, because 
> > the user can simply cast to an A object, and call A.foo(int) directly. 
> > Therefore, if the author meant to override foo(int) by defining just a 
> > foo(long), he has failed.  From these points, I believe that the above code 
> > is an example of an incorrectly implemented override, and I believe that the 
> > correct response the compiler should have is to error out, not while 
> > compiling test(), but while compiling B, indicating to the user that he 
> > should either declare the alias, or override foo(int).  This is the point in 
> > which my solution differs from Java.  Java would allow this to proceed, and 
> > call the base class' foo(int) in all cases, which is not what the author 
> > intended.
> 
> The next update of the compiler will throw a runtime exception for this 
> case.
> 
> > The second issue is how to handle the foo(char[]) case.  In the current 
> > implementation, because A is not searched for overrides, the compiler 
> > produces an error indicating that the user tried to call the foo(long) 
> > method, but there is no implicit conversion from char[] to long.  There are 
> > two possibilities.  One is that the author did not notice that A.foo(char[]) 
> > existed, and intended to override all instances of foo() with his foo(long) 
> > override.  However, the author is NOT notified of this issue, only the user 
> > of the class is notified of this issue.  So the potential for unambiguous 
> > code to have escaped exists.
> 
> But a compile time error is still generated, so I don't regard this as a 
> big problem. The big problems are silent hijacking of code.
> 
> 
> > The second possibility is that the author 
> > fully intended to allow the base class to define foo(char[]), but forgot to 
> > define the alias.  Again, since the compiler gives no error, he is unaware 
> > that he is releasing buggy code to the world.  I believe the correct 
> > assumption of the compiler should be that the user wanted the alias for the 
> > base class' foo(char[]), and should alias it implicitly if and only if no 
> > suitable match exists on the derived class.  In the case where the author 
> > did not notice foo(char[]) existed, he problably doesn't mind that 
> > foo(char[]) is defined by the base class.
> 
> The problem with code that looks like a mistake, but the compiler makes 
> some assumption about it and compiles it anyway, is that the code 
> auditor cannot tell if it was intended behavior or a coding error. 
> Here's a simple example:
> 
> void foo()
> { int i;
>    ...
>    { int i;
>      ...
>      use i for something;
>    }
> }
> 
> To a code auditor, that shadowing declaration of i looks like a mistake, 
> because possibly the "use i for something" code was meant to refer to 
> the outer i, not the inner one. (This can happen when code gets updated 
> by multiple people.) To determine if it was an actual mistake, the code 
> auditor is in for some serious spelunking. This is why, in D, shadowing 
> declarations are illegal. It makes life easier for the auditor, because 
> code that looks like a mistake is not allowed.
> 
> > If a suitable match exists that 
> > is not a direct override of the base class, then the issue reduces to the 
> > previous case, where an implicit conversion is required, and the compiler 
> > should error out.
> > 
> > There is one other possibile solution that I would be willing to concede to, 
> > and that is that the compiler errors out if the base class does not override 
> > all overloads of a particular method name.  This forces the user to either 
> > override all overloads of the method, or define the alias.  This would be 
> > the safest solution, as the author of B must make his intentions perfectly 
> > clear.
> 
> I am not comfortable with this method, as it will force the derived 
> class programmer to implement overloads that may not be at all meant to 
> exist in the API he defines for that class. I think he should be in full 
> control of the API for the class, and not forced to provide 
> implementations of functions that may be irrelevant clutter. I prefer 
> the solution where attempts to call unoverridden base class overloads 
> will result in a runtime exception.
> 
> > I believe this will give us the best of both camps, and allow much less code 
> > to be released with silent bugs than the current implementation.
> 
> I believe that the enforcing of the override attribute, and the runtime 
> exception case as described, closes all the known hijacking issues.

For the first example I think the exception solution will work fine. Since any predefined choice could hurt the work of the programmer.

For the second, where the argument can't be implicitily converted, I think that if the object in question doesn't have the required method, that method should be looked in the base classes, until it reaches the Object class. And it should do this without needing to declare an alias.

I think this is similar to how java works. And it's also similar to Regan sugestion.
August 07, 2007
Re: Proposition for change in D regarding Inheriting overloaded methods
Walter Bright wrote
> closes all the known hijacking issues.

If accessing data declared private in another not imported module can 
be called hijacking there is at least one more issue:


Defining a Base class:

module def;
class Base{int i;}


and defining a Derived class with some operations and some private data 
`hidden':

module def2;
import std.stdio;
import def;
class Derived:Base{
 private:
 int hidden=41;  // foreign access possible
 public:
 void process( inout Base p){
   scope d= new Derived;
   d.hidden= 666;
   d.i+=1;
   p= d; // no upcast needed
 }
 void read( Base p){
   scope d= cast(Derived)p;  //downcast needed
   assert( d !is null);
   writefln( "def2:", d.hidden);
 }
}


and having a module that does some work:

module mod;
private import std.stdio, def, def2;
public:
void process( inout Base p){
 scope d= new Derived;
 d.process= p; // stores Base, drops Derived
 // do something with d?
}
void read( Base p){
 scope d= new Derived;
 d.read= p; // drops Derived
 // do something with d?
}



the following claim is currently (dmd.1.016,win32) true::

! One can access the `private' data `hidden' defined in `module def2'
! by having access only to sources of `module def' and `module mod'.  


This are about 25 LOC and if D is indeed designed to support auditing 
well, it should be easy to spot that leak.

-manfred
August 07, 2007
Re: Proposition for change in D regarding Inheriting overloaded methods
Regan Heath wrote:
> Walter Bright wrote:
>> The next update of the compiler will throw a runtime exception for 
>> this case.
> 
> So, in this case:
> 
> class B
> {    long x;
>      void set(long i) { x = i; }
>     void set(int i) { x = i; }
>     long squareIt() { return x * x; }
> }
> class D : B
> {
>     long square;
>     void set(long i) { B.set(i); square = x * x; }
>     long squareIt() { return square; }
> }
> long foo(B b)
> {
>     b.set(3);
>     return b.squareIt();
> }
> 
> when the call to b.set(3) is made you insert a runtime check which looks 
> for methods called 'set' in <actual type of object>, if none of them 
> take a <insert types of parameters> you throw an exception.

There is no runtime check or cost for it. The compiler just inserts a 
call to a library support routine in D's vtbl[] entry for B.set(int).

> Is this done at runtime instead of compile time because the parameters 
> cannot always be determined at compile time?

Yes.

> 
>>> The second possibility is that the author fully intended to allow the 
>>> base class to define foo(char[]), but forgot to define the alias.  
>>> Again, since the compiler gives no error, he is unaware that he is 
>>> releasing buggy code to the world.  I believe the correct assumption 
>>> of the compiler should be that the user wanted the alias for the base 
>>> class' foo(char[]), and should alias it implicitly if and only if no 
>>> suitable match exists on the derived class.  In the case where the 
>>> author did not notice foo(char[]) existed, he problably doesn't mind 
>>> that foo(char[]) is defined by the base class.
>>
>> The problem with code that looks like a mistake, but the compiler 
>> makes some assumption about it and compiles it anyway, is that the 
>> code auditor cannot tell if it was intended behavior or a coding 
>> error. Here's a simple example:
>>
>> void foo()
>> { int i;
>>   ...
>>   { int i;
>>     ...
>>     use i for something;
>>   }
>> }
>>
>> To a code auditor, that shadowing declaration of i looks like a 
>> mistake, because possibly the "use i for something" code was meant to 
>> refer to the outer i, not the inner one. (This can happen when code 
>> gets updated by multiple people.) To determine if it was an actual 
>> mistake, the code auditor is in for some serious spelunking. This is 
>> why, in D, shadowing declarations are illegal. It makes life easier 
>> for the auditor, because code that looks like a mistake is not allowed.
> 
> It took me a while (because the example seems to be about something 
> totally different) but I think the argument you're making is that you 
> would prefer an error, requiring the author to specify what they want 
> explicitly, rather than for the compiler to make a potentially incorrect 
> assumption, silently. Is that correct?

Yes.

> 
> In the original example (trimmed slightly):
> 
> class A
> {
>    int foo(int x) { ... }
>    int foo(long y) { ... }
>    int foo(char[] s) { ... }
> }
> 
> class B : A
> {
>   override int foo(long x) { ... }
> }
> 
> void test()
> {
>   B b = new B();
>   A a = b;
> 
>   b.foo("hello");     // generates a compiler error
>   a.foo("hello");     // calls A.foo(char[])
> }
> 
> you're already making an assumption, you're assuming the author of B 
> does not want to expose foo(char[]) and it's the fact that this 
> assumption is wrong that has caused this entire debate.

The language is assuming things on the conservative side, not the 
expansive side, based on the theory that it is better to generate an 
error for questionable (and easily correctable) constructs than to make 
a silent (and erroneous) assumption.


> As others have mentioned, this assumption destroys the "is-a" 
> relationship of inheritance because "foo(char[])" is a method of A but 
> not a method of B.

We should not take rules as absolutes when they don't give us desirable 
behavior.


> Meaning B "isn't-a" A any more... unless you've 
> referring to a B with a reference to an A, when suddenly, it is.

That will generate a runtime error.

> Crazy idea, could the compiler (when it fails to match this overload) 
> cast the object to it's base class and try again, repeat until you hit 
> Object.  I guess this would essentially be a modification of the method 
> lookup rules ;)
> 
> 
> Making the opposite assumption (implicitly aliasing the "foo(char[])") 
> doesn't introduce any silent bugs (that I am aware of) and restores the 
> "is-a" relationship.
> 
> If the author really didn't want to expose "foo(char[])" then why were 
> they deriving their class from A?  It goes against the whole idea of 
> inheritance, doesn't it?

The problem is when the base class implementor wants to add some 
functionality (or specialization) with a new overload. A's implementor 
may be a third party, and has no idea about or control over B. His hands 
shouldn't be tied.
August 07, 2007
Re: Proposition for change in D regarding Inheriting overloaded methods
"Walter Bright" wrote in message news:f9aalt$b2p$1@digitalmars.com...
> Steven Schveighoffer wrote:
>> However, does the author intend to handle the case where foo(int) is 
>> called? Let's assume he does (which is what the current compiler 
>> assumes).  The author has not forbidden the user of the class from 
>> calling A.foo, because the user can simply cast to an A object, and call 
>> A.foo(int) directly. Therefore, if the author meant to override foo(int) 
>> by defining just a foo(long), he has failed.  From these points, I 
>> believe that the above code is an example of an incorrectly implemented 
>> override, and I believe that the correct response the compiler should 
>> have is to error out, not while compiling test(), but while compiling B, 
>> indicating to the user that he should either declare the alias, or 
>> override foo(int).  This is the point in which my solution differs from 
>> Java.  Java would allow this to proceed, and call the base class' 
>> foo(int) in all cases, which is not what the author intended.
>
> The next update of the compiler will throw a runtime exception for this 
> case.

How is this better than the current implementation?  In the current 
implementation, the code compiles and creates an obscure bug because the 
behavior isn't what the user expects.  In this new implementation, the 
obscure bug is only less obscure because an exception is thrown.  The code 
still compiles properly.  Why allow compilation at all?

>
>> The second issue is how to handle the foo(char[]) case.  In the current 
>> implementation, because A is not searched for overrides, the compiler 
>> produces an error indicating that the user tried to call the foo(long) 
>> method, but there is no implicit conversion from char[] to long.  There 
>> are two possibilities.  One is that the author did not notice that 
>> A.foo(char[]) existed, and intended to override all instances of foo() 
>> with his foo(long) override.  However, the author is NOT notified of this 
>> issue, only the user of the class is notified of this issue.  So the 
>> potential for unambiguous code to have escaped exists.
>
> But a compile time error is still generated, so I don't regard this as a 
> big problem. The big problems are silent hijacking of code.
>

The problem is WHEN the compile time error is generated.  I do not mind if 
the author of the class cannot generate object code for his class because of 
a compile time error (see my alternate solution).  However, because the 
compile error is generated when someone attempts to USE the class, the error 
is delivered to the incorrect person.  That person may not have the ability 
to fix the error.

Silent hijacking of code is not possible with the solution I propose, so 
that becomes a moot point.

>
>> The second possibility is that the author fully intended to allow the 
>> base class to define foo(char[]), but forgot to define the alias.  Again, 
>> since the compiler gives no error, he is unaware that he is releasing 
>> buggy code to the world.  I believe the correct assumption of the 
>> compiler should be that the user wanted the alias for the base class' 
>> foo(char[]), and should alias it implicitly if and only if no suitable 
>> match exists on the derived class.  In the case where the author did not 
>> notice foo(char[]) existed, he problably doesn't mind that foo(char[]) is 
>> defined by the base class.
>
> The problem with code that looks like a mistake, but the compiler makes 
> some assumption about it and compiles it anyway, is that the code auditor 
> cannot tell if it was intended behavior or a coding error.

I understand your point of view, and that is why I said that I would concede 
to a solution where a compiler error is thrown if all overloads are not 
handled.  However, I think it is still incorrect to generate the compiler 
error on the use of the class rather than the compilation of the class.  My 
preference as I said is to avoid having to specify the alias in the simple 
case where the arguments are not implicitly convertable, but if there must 
be a compilation error, I am willing to live with that.

>> If a suitable match exists that is not a direct override of the base 
>> class, then the issue reduces to the previous case, where an implicit 
>> conversion is required, and the compiler should error out.
>>
>> There is one other possibile solution that I would be willing to concede 
>> to, and that is that the compiler errors out if the base class does not 
>> override all overloads of a particular method name.  This forces the user 
>> to either override all overloads of the method, or define the alias. 
>> This would be the safest solution, as the author of B must make his 
>> intentions perfectly clear.
>
> I am not comfortable with this method, as it will force the derived class 
> programmer to implement overloads that may not be at all meant to exist in 
> the API he defines for that class. I think he should be in full control of 
> the API for the class, and not forced to provide implementations of 
> functions that may be irrelevant clutter. I prefer the solution where 
> attempts to call unoverridden base class overloads will result in a 
> runtime exception.
>

The problem is they ARE implemented, and now they will cause runtime 
exceptions!  Also, now this breaks the contract that the base class 
provides.  If you give me an instance of a certain class, and the 
documentation for that class says that it defines a given method, then that 
method should be implemented in all derivatives.  If you want to derive a 
class and force the implementation of a base class' method to throw an 
exception, I think that should be the (for better lack of a word) exception, 
not the rule.  Why derive from a class where you want to shoehorn its 
functionality into something different?  I would recommend to someone trying 
to do that to define a new class, rather than derive.  I challenge anyone to 
give a real-world example of why this should be possible.

As you point out, it is possible to force the implementation that you are 
specifying by overriding the overloaded method and throwing the exception 
yourself, and maybe the requirement of extra cluttering functions will 
discourage people from implementing their code this way.  Maybe there could 
be a specific keyword or something to specify that you want to have the 
overload throw an exception, so there is less code clutter, but I do not 
think the default should be throwing an exception.

-Steve
August 07, 2007
Re: Proposition for change in D regarding Inheriting overloaded methods
"Walter Bright" wrote
> Regan Heath wrote:
>> when the call to b.set(3) is made you insert a runtime check which looks 
>> for methods called 'set' in <actual type of object>, if none of them take 
>> a <insert types of parameters> you throw an exception.
>
> There is no runtime check or cost for it. The compiler just inserts a call 
> to a library support routine in D's vtbl[] entry for B.set(int).
>
>> Is this done at runtime instead of compile time because the parameters 
>> cannot always be determined at compile time?
>
> Yes.

Hm... I'm slightly ignorant on this issue, not being a compiler developer. 
After reading this, I'm thinking I need to change my proposition to my 
alternate solution, which is that the compiler should produce an error 
whenever the derived class does not override the base class's overloads (my 
alternate solution).  From your answer here, it appears that my assumption 
that the compiler can tell whether a given type of argument could be 
converted to a base class' argument type might be impossible to tell at 
compile time.  Or is it?  In any case, now that I think about it, it 
produces an O(n^2) run time as the compiler needs to check every argument 
type in the derived class to see if it can be implicitly converted to every 
argument type in the base class.  This might produce a very slow compiler.

I still believe generating a runtime error is no better than the current 
behavior of the compiler, actually I think it's worse.

>> In the original example (trimmed slightly):
>>
>> class A
>> {
>>    int foo(int x) { ... }
>>    int foo(long y) { ... }
>>    int foo(char[] s) { ... }
>> }
>>
>> class B : A
>> {
>>   override int foo(long x) { ... }
>> }
>>
>> void test()
>> {
>>   B b = new B();
>>   A a = b;
>>
>>   b.foo("hello");     // generates a compiler error
>>   a.foo("hello");     // calls A.foo(char[])
>> }
>>
>> you're already making an assumption, you're assuming the author of B does 
>> not want to expose foo(char[]) and it's the fact that this assumption is 
>> wrong that has caused this entire debate.
>
> The language is assuming things on the conservative side, not the 
> expansive side, based on the theory that it is better to generate an error 
> for questionable (and easily correctable) constructs than to make a silent 
> (and erroneous) assumption.

The conservative side would be to say that it is an error to generate the 
class in the first place seeing as how it hasn't defined all the behavior it 
should.  The more I look at it, I think the best solution is not to assume 
anything, and force the author of the class to define it more clearly.

>
>
>> As others have mentioned, this assumption destroys the "is-a" 
>> relationship of inheritance because "foo(char[])" is a method of A but 
>> not a method of B.
>
> We should not take rules as absolutes when they don't give us desirable 
> behavior.
>

I still would like a real example of how this is desirable.


>> If the author really didn't want to expose "foo(char[])" then why were 
>> they deriving their class from A?  It goes against the whole idea of 
>> inheritance, doesn't it?
>
> The problem is when the base class implementor wants to add some 
> functionality (or specialization) with a new overload. A's implementor may 
> be a third party, and has no idea about or control over B. His hands 
> shouldn't be tied.

This argument is absurd.  How is the author of A's hands tied?  If author A 
changes his API, author B had better take notice.  What if author A decides 
to change the way he stores protected data?  How can you prevent author B 
from having to understand that?

Bottom line, if you derive a class, and the base class changes, all bets are 
off.  You may have to change your class.  I see no way to prevent this 
possibility.

Even with your exception solution, A can break code which uses instances of 
B by adding an overload.

-Steve
August 07, 2007
Re: Proposition for change in D regarding Inheriting overloaded methods
Regan Heath wrote:
> Walter Bright wrote:
>> The next update of the compiler will throw a runtime exception for 
>> this case.
> 
> So, in this case:
> 
> class B
> {    long x;
>      void set(long i) { x = i; }
>     void set(int i) { x = i; }
>     long squareIt() { return x * x; }
> }
> class D : B
> {
>     long square;
>     void set(long i) { B.set(i); square = x * x; }
>     long squareIt() { return square; }
> }
> long foo(B b)
> {
>     b.set(3);
>     return b.squareIt();
> }
> 
> when the call to b.set(3) is made you insert a runtime check which looks 
> for methods called 'set' in <actual type of object>, if none of them 
> take a <insert types of parameters> you throw an exception.

I assume the compiler would not throw an exception for the following case?

class D : B
{
    long square;
    alias B.set set;
    void set(long i) { B.set(i); square = x * x; }
    long squareIt() { return square; }
}


Sean
« First   ‹ Prev
1 2 3
Top | Discussion index | About this forum | D home