Jump to page: 1 2
Thread overview
How should overruling of overloaded functions work?
Aug 19, 2006
Kristian
Aug 20, 2006
Søren J. Løvborg
Aug 20, 2006
Kristian
Aug 20, 2006
nobody
Aug 20, 2006
Søren J. Løvborg
Aug 21, 2006
nobody
Aug 21, 2006
Søren J. Løvborg
Aug 25, 2006
Bruno Medeiros
Aug 20, 2006
Kristian
Aug 22, 2006
Bruno Medeiros
Aug 22, 2006
Kristian
August 19, 2006
Ok, this issue is controversaly. The following explains the situation fine:

http://groups.google.com/group/alt.comp.lang.learn.c-c++/msg/2bdda463aed5d153?hl=en&lr=lang_en&ie=UTF-8&oe=UTF-8&safe=off

In short, should the following work (without using the alias hack):

class Base {
    void f() {...}
    void f(int) {...}
}

class Derived : Base {
    void f() {...}
}

void func() {
    Derived obj = new Derived;

    obj.f(10);  //doesn't work
}

There were/are good reason(s) why it should not work (see the link).
I personally think it should work, though. There are a lot of people that think likewise. Of course, the other half think that it shouldn't work.


This is why I think it should work:

1)
If you like to change the behavior of the function altogether, then you should change all the overload functions. If a new overload is later added to the Base class, then you have to add the corresponding function to the Derived class. That is a problem, but hiding of overloads of Base doesn't work either. Why? See the point 2.


2)
Using the Derived class via the Base class will access the overloads defined in Base. For example:

class Base {
    void f(int v) {printf("%d\n", v);}
    void f(float v) {printf("%f\n", v);}
}

class Derived : Base {
    void f(int v) {printf("%d\n", -v);}
}

void func(Base obj) {
    obj.f(1);     //prints -1
    obj.f(1.0);   //prints 1.0, not -1.0
}

void main() {
    Derived obj = new Derived;

    func(obj);
}

We want that Derived changes the behavior of 'f()' so that it'll print positive values as negative ones and vice versa. If 'f()' is used in 'main', the hiding of Base's 'f(float)' will prevent it being called. However, this restriction does not apply to inside 'func()'. 'f()' works incorrectly here: it prints a positive value for a float, which is not wanted. The programmer of course wants that 'f()' works consistent _throughout_ the code.

When 'f(float)' is added to Base, the corresponding function must also be added to Derived.


3)
If the overloading does not work for derived classes, then the following common case does not work without the alias hack:

class String {...}

class Base {
    void f(String s) {...}
    //these functions are provided for convenience...
    final void f(int v) {
        f(new String(v));
        }
    final void f(float v) {
        f(new String(v));
        }
}

class Derived : Base {
    void f(String s) {...}  //overrule the main function that does all the work
}

'f(int)' and 'f(float)' are hidden from Derived. You have to use aliasing, or the following coding style:

class Base {
    final void f(String s) {
        do_f(s);
        }
    final void f(int v) {
        do_f(new String(v));
        }
    final void f(float v) {
        do_f(new String(v));
        }

    void do_f(String s) {...}
}

class Derived : Base {
    void do_f(String s) {...}
}


4)
Aliasing nullifies the 'safety net' provided by the language. (Hence the safety net does not work.)


5)
The hiding of overloads in Derived works only partially, which is not a good thing, I think. It should work completely or not at all. In addition, cases where the hiding is not desirable are far more common (IMHO).
August 20, 2006
This is the old problem of modifying the base class, while having the subclass continue to work as it should. It manifests itself in many ways.

The only way one can be sure to avoid this problem, is to never extend a class that one does not control -- i.e. classes defined in libraries etc.

If this rule is followed, there's indeed no reason for the current (C++ like) hiding of overloaded functions.

See for instance, "Why extends is evil" at JavaWorld, discussing the "fragile base-class" problem. http://www.javaworld.com/javaworld/jw-08-2003/jw-0801-toolbox.html

Quote:
> I once attended a Java user group meeting where James Gosling (Java's inventor) was the featured speaker. During the memorable Q&A session, someone asked him: "If you could do Java over again, what would you change?" "I'd leave out classes," he replied.

It's a controversial topic. :-)

Søren J. Løvborg
web@kwi.dk


August 20, 2006
On Sun, 20 Aug 2006 14:54:29 +0300, Søren J. Løvborg <web@kwi.dk> wrote:
> This is the old problem of modifying the base class, while having the
> subclass continue to work as it should. It manifests itself in many ways.
>
> The only way one can be sure to avoid this problem, is to never extend a
> class that one does not control -- i.e. classes defined in libraries etc.
>
> If this rule is followed, there's indeed no reason for the current (C++
> like) hiding of overloaded functions.
>
> See for instance, "Why extends is evil" at JavaWorld, discussing the
> "fragile base-class" problem.
> http://www.javaworld.com/javaworld/jw-08-2003/jw-0801-toolbox.html
>
> Quote:
>> I once attended a Java user group meeting where James Gosling
>> (Java's inventor) was the featured speaker. During the memorable
>> Q&A session, someone asked him: "If you could do Java over again,
>> what would you change?" "I'd leave out classes," he replied.
>
> It's a controversial topic. :-)
>
> Søren J. Løvborg
> web@kwi.dk
>

Thanks for the link to the article, it was interesting. There were good points, although inheritance was designed so that you can re-use the old code, of course. Inheritance is a good thing, but it can also be a bad thing. I guess there are two sides in every programming style, rules, etc.
August 20, 2006
On Sat, 19 Aug 2006 18:09:25 +0300, Kristian <kjkilpi@gmail.com> wrote:
> 3)
> If the overloading does not work for derived classes, then the following common case does not work without the alias hack:
>
> class String {...}
>
> class Base {
>      void f(String s) {...}
>      //these functions are provided for convenience...
>      final void f(int v) {
>          f(new String(v));
>          }
>      final void f(float v) {
>          f(new String(v));
>          }
> }
>
> class Derived : Base {
>      void f(String s) {...}  //overrule the main function that does all the work
> }


Okey, okey, why doesn't someone tell me that this actually works!? :)

The functions 'f(int)' and 'f(float)' are not hidden from Derived because they're final. Nice!

This was the main reason I protested against the current overload function hiding. But because it do work, I will fell silent (and look a bit embarrassed). ;)

This enables me to write these convenience functions without the need of rewriting (or alias hacking) them in subclasses. Non-final functions should be reimplemented anyway (if someone wants to use alias hacking for those, be my guest).
August 20, 2006
Søren J. Løvborg wrote:
> See for instance, "Why extends is evil" at JavaWorld, discussing the "fragile base-class" problem.
> http://www.javaworld.com/javaworld/jw-08-2003/jw-0801-toolbox.html

That article was horrible. It sounds plausible that safe changes in a base class might have undesired effects on a derived class but I would need an example. All his examples ignored the stubs of the base class. Big surprise ending -- he fixes the problem of ignoring the stubs of base classes with an interface.

He says "whenever I violate a central OO principle like *implementation hiding*, I end up rewriting that code." Yet the article is really about subtle variations of failing to hide implementation. So subtle in fact that the author did not even realize the problem:

  class ObviousProblem
  {
    public DataStore data;
  }

  class SubtleProblem extends DataStore
  {
    // does not override /any/ DataStore methods
  }

Now we have:

  ObviousProblem o = new ObviousProblem();
  o.data.invalidate();

  SubtleProblem  s =  new SubtleProblem();
  s.invalidate();

In short extending a class just because you are using it as a backing store is a bad idea. A big clue that is-a does not hold is when not a single one of your base class' methods make sense for the derived class. Yet despite adding and removing random elements makes no sense for a Stack his example is:

  class ArrayList
  {
    public void add( int i, Object o ) { }
    public Object remove( int i ) { }
    public void clear() { }
  }

  class Stack extends ArrayList
  {
    public void push( Object article )
    public Object pop()
    public void push_many( Object[] articles )
  }

Now translating that to a more obvious example an implementation hiding failure:

  class StackEquivalent
  {
    public ArrayList data;
    public void push( Object article )
    public Object pop()
    public void push_many( Object[] articles )
  }

Using /any/ of ArrayList's functions on a Stack object will invalidate it. Not just clear(). The /exact/ same situation applies to StackEquivalent.



Now the second implementation hiding failutre is not about exposing more than one needs but rather less:

  class Stack
  {
    public void push( Object article )
    public Object pop()
    public void push_many( Object[] articles )
  }

  class Monitorable_stack extends Stack
  {
      private int high_water_mark = 0;
      private int current_size;

      public void push( Object article )
      {   if( ++current_size > high_water_mark )
              high_water_mark = current_size;
          super.push(article);
      }

      public Object pop()
      {   --current_size;
          return super.pop();
      }

      public int maximum_size_so_far()
      {   return high_water_mark;
      }
  }

What jumps out when you just look at the function stubs for base and derived is that Monitorable_stack chose not to override push_many. It certainly is one of the functions you ought to be interested in intercepting.

The only plausible conclusion is that an inference has been made about Stack's hidden implementation. It turns out that was exactly the reasoning behind the article's author. Yet again has to violate "a central OO principle like implementation hiding" in order to make his point.

Given the pattern that all his examples ignored the stubs of the base class one can only conclude that he needs to use interfaces to be able to think clearly about what certain classes expose. If it works for him then I am happy he figured it out. However his stub blindness has nothing fundamental to do with inheritance. In short I suspect he has misinterpreted "The fragile base-class problem".
August 20, 2006
> What jumps out when you just look at the function stubs for base and derived is that Monitorable_stack chose not to override push_many. It certainly is one of the functions you ought to be interested in intercepting.
>
> The only plausible conclusion is that an inference has been made about Stack's hidden implementation. It turns out that was exactly the reasoning behind the article's author. Yet again has to violate "a central OO principle like implementation hiding" in order to make his point.

Imagine that push_many was added to the base class in a later version, after Stack was created. Then you have the exact same problem, without making any assumptions about the implementation as such. And the same problem is used to justify the hiding of overloaded methods in C++ (and hence, D), as discussed in the original post.

The difference between extending a base class and implementing an interface is that if you add a new method to the class, it can introduce subtle bugs, while if you add it to an interface, it's a compile-time error unless you also add it to every implementation of that interface.

The reason for extending a class is to override "hook" methods to change the functionality of the class, e.g. the paint() method of a GUI control, or the add() method of a list. However, when you do this, you no longer only have to worry about the "public interface" for the class (all the public methods, which users of the class may call), but also about keeping a consistent "protected interface".

In the public interface, the implementor simply marks the public methods with visibility "public", and he's done.

    /* Call this to add an object onto the stack. */
    public void push(Object o)

The contract for using the public interface is simple: Provide valid argument values, and the method will do what it's supposed to.

With the "protected interface", correct visibility is the least of the problems. The implementor needs to define exactly which methods may be overriden by subclasses (not neccessarily all of the public methods), and the exact semantics of each method, for instance:

    /* This is guaranteed to be called exactly once for every object added.
*/
    public void push(Object o)

The contract for using the "protected interface" isn't simple. When subclassing, you need to read the documentation, or you'll introduce hard to find bugs. The contract cannot be enforced by the compiler, nor can it be enforced at runtime.

So yes, you can produce code that works perfectly, in which you extend
classes (obviously -- everybody does it).
You can also produce code that works perfectly, in which the member
variables of all classes are public.
By eliminating or minimizing both, you might save yourself some trouble. Or
you might not.

I, personally, am not letting go of subclasses just yet...

Søren J. Løvborg
web@kwi.dk


August 21, 2006
Søren J. Løvborg wrote:
>> What jumps out when you just look at the function stubs for base and derived is that Monitorable_stack chose not to override push_many. It certainly is one of the functions you ought to be interested in intercepting.
>>
>> The only plausible conclusion is that an inference has been made about Stack's hidden implementation. It turns out that was exactly the reasoning behind the article's author. Yet again has to violate "a central OO principle like implementation hiding" in order to make his point.
> 
> Imagine that push_many was added to the base class in a later version, after Stack was created. Then you have the exact same problem, without making any assumptions about the implementation as such. And the same problem is used to justify the hiding of overloaded methods in C++ (and hence, D), as discussed in the original post.
> 
> The difference between extending a base class and implementing an interface is that if you add a new method to the class, it can introduce subtle bugs, while if you add it to an interface, it's a compile-time error unless you also add it to every implementation of that interface.
> 
> The reason for extending a class is to override "hook" methods to change the functionality of the class, e.g. the paint() method of a GUI control, or the add() method of a list. However, when you do this, you no longer only have to worry about the "public interface" for the class (all the public methods, which users of the class may call), but also about keeping a consistent "protected interface".
> 
> In the public interface, the implementor simply marks the public methods with visibility "public", and he's done.
> 
>     /* Call this to add an object onto the stack. */
>     public void push(Object o)
> 
> The contract for using the public interface is simple: Provide valid argument values, and the method will do what it's supposed to.
> 
> With the "protected interface", correct visibility is the least of the problems. The implementor needs to define exactly which methods may be overriden by subclasses (not neccessarily all of the public methods), and the exact semantics of each method, for instance:
> 
>     /* This is guaranteed to be called exactly once for every object added. */
>     public void push(Object o)
> 
> The contract for using the "protected interface" isn't simple. When subclassing, you need to read the documentation, or you'll introduce hard to find bugs. The contract cannot be enforced by the compiler, nor can it be enforced at runtime.
> 
> So yes, you can produce code that works perfectly, in which you extend classes (obviously -- everybody does it).
> You can also produce code that works perfectly, in which the member variables of all classes are public.
> By eliminating or minimizing both, you might save yourself some trouble. Or you might not.
> 
> I, personally, am not letting go of subclasses just yet...
> 
> Søren J. Løvborg
> web@kwi.dk 
> 
> 

Thanks for the eloquent and well thought out reply. I realized I was out of line with my criticism of his second example. From this article a damning quote:

> Bill Venners:
>
> In Effective Java, Bloch offered a good example in which the addAll method
> in class HashSet calls add on itself. A subclass attempts to track the total
> number of objects added to the set by counting the objects passed to both
> addAll and add, but it doesn't work as expected because of addAll's self-use.
> If someone passes three objects to addAll, the total number of objects
> increments by six, not three, because addAll calls add three times.
>
> http://www.artima.com/intv/issues2.html

In the previous example he only counts in the single add case but in this example counting in both cases is incorrect. The correct behavior seems to be only possible either by violating the implementation hiding and peeking or somehow getting Stack to also grow a length() function magically!

I would certainly quantify this combination of factors as one nasty example of safe changes in a base class having undesired effects on a derived class. Certainly as you suggested in your first post adding a length function magically is actually pretty easy if you only subclass your own classes.

I am also left wondering how often this concern actually manifests itself. Is it purely theory with no practice? Would Sun silently slip in something like an addAll function to HashSet later? How much has the C library changed over time? How relevent is C's past to the future of more recent languages?
August 21, 2006
> I am also left wondering how often this concern actually manifests itself. Is it purely theory with no practice? Would Sun silently slip in something like an addAll function to HashSet later? How much has the C library changed over time? How relevent is C's past to the future of more recent languages?

Well, as of Java 1.2, with the introduction of the unified Collection framework, the addElement() method of Vector was superseded by a new method, add().

addElement() stays, however, for backwards compatibility, and isn't even deprecated (for reasons only known to Sun...)

Unless add() simply redirects to addElement(), old code that only overrides addElement() will be broken.

A quick test (with Sun's Java 1.6 beta) shows that in fact, neither method calls the other one. Old code could indeed be broken by this.

So, yes, such library changes happens. Which I guess is sort of an argument for retaining the C++ overload hiding rules. (Yet, I still think they're unneccessary.)

Søren J. Løvborg
web@kwi.dk


August 22, 2006
Kristian wrote:
> On Sat, 19 Aug 2006 18:09:25 +0300, Kristian <kjkilpi@gmail.com> wrote:
>> 3)
>> If the overloading does not work for derived classes, then the following common case does not work without the alias hack:
>>
>> class String {...}
>>
>> class Base {
>>      void f(String s) {...}
>>      //these functions are provided for convenience...
>>      final void f(int v) {
>>          f(new String(v));
>>          }
>>      final void f(float v) {
>>          f(new String(v));
>>          }
>> }
>>
>> class Derived : Base {
>>      void f(String s) {...}  //overrule the main function that does all the work
>> }
> 
> 
> Okey, okey, why doesn't someone tell me that this actually works!? :)
> 
> The functions 'f(int)' and 'f(float)' are not hidden from Derived because they're final. Nice!
> 
> This was the main reason I protested against the current overload function hiding. But because it do work, I will fell silent (and look a bit embarrassed). ;)
> 
> This enables me to write these convenience functions without the need of rewriting (or alias hacking) them in subclasses. Non-final functions should be reimplemented anyway (if someone wants to use alias hacking for those, be my guest).

Are you sure it works? It's not working for me, I tried the following code:

-----
import std.stdio;

class String {}

class Base {
    void f(String s) {
    	writefln("Base.f(String)");
    }
    //these functions are provided for convenience...
    final void f(int v) {
        writefln("Base.f(int)");
    }
    final void f(float v) {
        writefln("Base.f(float)");
    }
}

class Derived : Base {
    void f(String s) { writefln("Derived.f"); }
}

void test() {
    Derived obj = new Derived;
    obj.f(10);  // ERROR, doesn't work
}

August 22, 2006
OOOKAY! You're right, that does not work! :( :( :(

There was a stupid error in my test case... and I was too quick to get out of my house running and yelling "it works!" to people! (When police appeared, I was forced to stop.)

One word: aaargh!
It was too good to be true anyway... :(

I just don't get it.
This is clearly a case where overload functions should not be hidden! I mean, if a function is final, you have to be absolutely sure that there is no need to change (i.e. override) it in a subclass.

If you cannot change it in a subclass, then there is no harm in changing its implementation in the base class. Subclasses will work as normal after the change. If not, then the function must not be final!


Thank you Walter, and others, for this great language.
... But *please* don't left these kind of glitches in it! Little details are important also, programmers have to deal with them all the time!


On Tue, 22 Aug 2006 17:15:56 +0300, Bruno Medeiros <brunodomedeiros+spam@com.gmail> wrote:
> Kristian wrote:
>> On Sat, 19 Aug 2006 18:09:25 +0300, Kristian <kjkilpi@gmail.com> wrote:
>>> 3)
>>> If the overloading does not work for derived classes, then the following common case does not work without the alias hack:
>>>
>>> class String {...}
>>>
>>> class Base {
>>>      void f(String s) {...}
>>>      //these functions are provided for convenience...
>>>      final void f(int v) {
>>>          f(new String(v));
>>>          }
>>>      final void f(float v) {
>>>          f(new String(v));
>>>          }
>>> }
>>>
>>> class Derived : Base {
>>>      void f(String s) {...}  //overrule the main function that does all the work
>>> }
>>   Okey, okey, why doesn't someone tell me that this actually works!? :)
>>  The functions 'f(int)' and 'f(float)' are not hidden from Derived because they're final. Nice!
>>  This was the main reason I protested against the current overload function hiding. But because it do work, I will fell silent (and look a bit embarrassed). ;)
>>  This enables me to write these convenience functions without the need of rewriting (or alias hacking) them in subclasses. Non-final functions should be reimplemented anyway (if someone wants to use alias hacking for those, be my guest).
>
> Are you sure it works? It's not working for me, I tried the following code:
>
> -----
> import std.stdio;
>
> class String {}
>
> class Base {
>      void f(String s) {
>      	writefln("Base.f(String)");
>      }
>      //these functions are provided for convenience...
>      final void f(int v) {
>          writefln("Base.f(int)");
>      }
>      final void f(float v) {
>          writefln("Base.f(float)");
>      }
> }
>
> class Derived : Base {
>      void f(String s) { writefln("Derived.f"); }
> }
>
> void test() {
>      Derived obj = new Derived;
>      obj.f(10);  // ERROR, doesn't work
> }
>

« First   ‹ Prev
1 2