8

I have been getting Delphi compiler warnings about Method 'Create' hides virtual method of base.

I have reviewed several Stack Overflow links (see below), and I don't understand the logic behind this warning, and why it is considered bad coding practice. I'm hoping others can help me understand

I will include some sample code:

type    
  TMachine = class(TPersistent)
  private
  public
    Horsepower : integer;
    procedure Assign(Source : TMachine);
  end;

...

procedure TMachine.Assign(Source : TMachine);
begin
  inherited Assign(Source);
  Self.Horsepower := Source.HorsePower;
end;

This causes the compiler warning.

[dcc32 Warning] Unit1.pas(21): W1010 Method 'Assign' hides virtual method of base type 'TPersistent'

I have been ignoring this warning because it didn't make any sense to me. But that got me in trouble in another way (see my other post here: Why does Delphi call incorrect constructor during dynamic object creation?) so I have decided to try to understand this better.

I know that if I use the reserved word reintroduce, the error will go away, but I have seen it repeatedly posted that this is a bad idea. As Warren P wrote here (Delphi: Method 'Create' hides virtual method of base - but it's right there), "IMHO, if you need reintroduce, your code smells horrible".

I think I understand what is meant by "hiding". As David Heffernan said here (What causes "W1010 Method '%s' hides virtual method of base type '%s'" warning?):

What is meant by hiding is that from the derived class you no longer have access to the virtual method declared in the base class. You cannot refer to it since it has the same name as the method declared in the derived class. And that latter method is the one that is visible from the derived class.

But I am somewhat confused because it seems that ancestor method is not really hidden, because a derived class can always just use the inherited keyword to call the method in the base class. So 'hidden' really means 'somewhat hidden'?

I think I also understand that using the reserved word override will prevent the compiler warning, but the procedure signature has to be the same (i.e. no newly added parameters). That I can't use that here.

What I don't understand is why hiding is something to be warned about. In my code example above, I would not want users of TMachine.Assign() to instead somehow use TPersistent.Assign(). In my extended class, I have extended needs, and therefore want to them to use the new and improved function. So it seems like hiding the older code is exactly what I want. My understanding of a virtual method is one where the correct method is called based on the actual type of an object at run time. I don't think that should have any bearing in this case.

Additional code, to be added to example code above

  TAutomobile = class(TMachine)
  public
    NumOfDoors : integer;
    constructor Create(NumOfDoors, AHorsepower : integer);
  end;

...

constructor TAutomobile.Create(ANumOfDoors, AHorsepower : integer);
begin
  Inherited Create(AHorsepower);
  NumOfDoors := ANumOfDoors;
end;

This adds new compiler warning message: [dcc32 Warning] Unit1.pas(27): W1010 Method 'Create' hides virtual method of base type 'TMachine'

I especially don't understand problems that arise with using new constructors with additional parameters. In this post (SerialForms.pas(17): W1010 Method 'Create' hides virtual method of base type 'TComponent'), the wisdom seems to be that a constructor with a different name should be introduced, e.g. CreateWithSize. This would seem to allow users to pick and choose which constructor they want to use.

And if they choose the the old constructor the extended class might be missing some needed information for creation. But if, instead, I 'hide' the prior constructor, it is somehow bad programming. Marjan Venema wrote about reintroduce in this same link: Reintroduce breaks polymorphism. Which means that you can no longer use meta classes (TxxxClass = class of Tyyy) to instantiate your TComponent descendant as its Create won't be called. I don't understand this at all.

Perhaps I need to understand polymorphism better. Tony Stark wrote in this link (What is polymorphism, what is it for, and how is it used?) that polymorphism is: "the concept of object oriented programming.The ability of different objects to respond, each in its own way, to identical messages is called polymorphism." So am I presenting a different interface, i.e. no longer an identical message, and thus this breaks polymorphism?

What am I missing? In summary, isn't hiding base code a good thing in my examples?

halfer
  • 19,824
  • 17
  • 99
  • 186
kdtop
  • 541
  • 1
  • 7
  • 22
  • 1
    Long story short, it's just making sure you know what you're doing. If you do, you can hide it by adding `reintroduce;` at the end to hide the warning. I personally don't understand why anyone says `reintroduce` is bad practice, I've seen it in the RTL itself. – Jerry Dodge May 26 '17 at 22:33
  • 2
    @Jerry: Seeing it in the VCL/RTL does not mean it's good code or practice. There is a boatload of bad code in the Delphi source. – Ken White May 27 '17 at 00:12
  • 1
    @Ken I can agree with that. I still don't see the harm in using `reintroduce` though, there have been situations when I had no other choice - when I really did need to implement something differently. That's especially true in some constructors. – Jerry Dodge May 27 '17 at 00:17
  • 2
    @Jerry: I didn't say there was anything wrong with using `reintroduce`. I just pointed out that *I've seen it in the RTL itself* is not meaningful. :-) – Ken White May 27 '17 at 00:22
  • 1
    On that note, I mentioned especially in constructors. A `TThread` is a perfect example. `TThread.Create` is almost never implemented with the same parameters. Instead, I make my own constructor, in which case I also add reintroduce to it in order to hide the compiler warning. – Jerry Dodge May 27 '17 at 00:36
  • 1
    In fact, a `TThread` forces you to override its `Execute` method, and since it's `abstract`, it also raises an exception if you don't inherit it. Obviously, since the whole base of a thread begins with inherited implementation of `Execute`. You never call `Execute` yourself. Instead, it's called from inside. Therefore, if you never used `override`, then it would be impossible for it to call *your* code. This compiler warning also alerts you of situations like that. – Jerry Dodge May 27 '17 at 00:43
  • 1
    _The ability of different objects to respond, each in its own way, to identical messages is called polymorphism._ - this is just plain wrong. –  May 27 '17 at 00:44
  • 1
    @RawN In the context of Delphi, sure, the term has more specific meaning. But that is a good definition for an overall scope of the term. For example, don't expect the word "messages" to mean "Windows messages". That definition covers all angles. – Jerry Dodge May 27 '17 at 00:45
  • 1
    Also, "hidden" does not mean invisible. It just means that a direct call to it will not work as originally intended. As I said before, long story short, `reintroduce` only hides the compiler warning, as a way to make sure you know what you're doing. – Jerry Dodge May 27 '17 at 01:26
  • 1
    reintroduce is a hack. It allows you to break polymorphism in a hierarchy and shut the compiler – Agustin Ortu May 27 '17 at 03:45
  • 2
    @Jerry You are quite wrong about TThread. Its constructors are not virtual. – David Heffernan May 27 '17 at 06:18

2 Answers2

6

The danger here is that you might call Assign on a base class reference. Because you did not use override then your derived class method is not called. You have thus subverted polymorphism.

By the principle of least surprise you should use override here, or give your derived class method a different name. The latter option is simple. The former looks like this:

type    
  TMachine = class(TPersistent)
  public
    Horsepower : integer;
    procedure Assign(Source : TPersistent); override;
  end;

...

procedure TMachine.Assign(Source : TPersistent);
begin
  if Source is TMachine then begin
    Horsepower := TMachine(Source).Horsepower;
  end else begin
    inherited Assign(Source);
  end;
end;

This allows your class to co-operate with the polymorphic design of TPersistent. Without using override that would not be possible.

Your next example, with virtual constructors is similar. The entire point of making a constructor virtual is so that you can create instances without knowing their type until runtime. The canonical example is the streaming framework, the framework that processes .dfm/.fmx files and creates objects and sets their properties.

That streaming framework relies on the virtual constructor of TComponent:

constructor Create(AOwner: TComponent); virtual;

If you want a component to work with the streaming framework, you must override this constructor. If you hide it, then the streaming framework cannot find your constructor.

Consider how the streaming framework instantiates components. It does not know about all the component classes it needs to work with. It cannot, for instance consider third party code, the code you write. The Delphi RTL cannot know about types defined there. The streaming framework instantiates components like this:

type
  TComponentClass = class of TComponent;

var
  ClassName: string;
  ClassType: TComponentClass;
  NewComponent: TComponent;

....
ClassName := ...; // read class name from .dfm/.fmx file
ClassType := GetClass(ClassName); // a reference to the class to be instantiated
NewComponent := ClassType.Create(...); // instantiate the component

The ClassType variable holds a meta class. This allows us to represent a type which is not known until runtime. We need the call to Create to be dispatched polymorphically so that the code in the component's constructor is executed. Unless you use override when declaring that constructor, it won't be.

Really, all of this boils down to polymorphism. If your understanding of polymorphism is not firm, as you suggest, then you will struggle to appreciate any of this. I think your next move is to get a better grip on what polymorphism is.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
2

There are different benefits for using inheritance. In your examples you do it to avoid coding the same things again and again. So if TMachine has Horsepower field already and some methods and now you need more advanced TAutomobile with NumOfDoors, you make it TMachine descendant.

If you now always treat them differently, i.e in some code you use exactly TMachine (machine := TMachine.Create(...), machine.Assign(AnotherMachine) etc. ) and in another code you use TAutomobile and they never get mixed then you're all right, you can ignore these warnings or 'mute' them with reintroduce.

But there is usually another aspect of inheritance: keeping uniform interface, or as it's sometimes called: 'contract'. Separating interface from implementation.

For example, form is able to free all the objects which belong to it, no matter what these objects are, that's because of Destroy method which gets overrided. Form doesn't care about your implementation, but it knows: to free the object it just have to call Destroy, that easy. If you don't override Destroy, that's extremely bad: no way TForm will call you as TMachine.Destroy. It'll call you as TObject.Destroy, but it won't lead to your TMachine.Destroy, so you get a memory leak. In most cases when some method wasn't overriden it's just because programmer forgot to do it, thus a warning: it's very helpful one. If programmer didn't forget it but that was intentionally, reintroduce keyword is used. This way programmer tells: "Yes, I know what I do, this is intentionally, don't disturb me!"

TPersistent.Assign is another procedure which is frequently called from base class, not derived (that is: we don't want to pay attention to implementation, we just want to copy an object, whatever it is). For example, TMemo has Lines: TStrings, but TStrings is an abstract class, while the actual implementation is TStringList. So, when you write Memo1.Lines.Assign(Memo2.Lines), the TStrings.Assign method is used. It may implement this assign through another methods: clear itself first and then add line after line. Some TStrings descendant may want to speed-up process by some block copy of data. Of course it has to use exactly Assign(Source: TPersistent) method and override it, otherwise it is never called (inherited is called instead).

Classic implementation of Assign is like this:

procedure TMachine.Assign(Source : TPersistent);
begin
  if Source is TMachine then
    Horsepower := TMachine(Source).Horsepower
  else inherited Assign(Source);
end;

That's the case when inherited shouldn't be called first thing. Here it is 'the last resort': it's called last if nothing else helped. It makes one final try: if your class don't know how to assign, maybe that Source knows how to AssignTo your class?

For example, TBitmap was coded long, long ago. After that TPngImage was developed to work with, well, PNG. You want to put PNG into bitmap and write: Bitmap.Assign(PngImage). No way TBitmap may know how to deal with PNG: it didn't exist back then! But TPngImage writer knew that may happen and implemented AssignTo method which is able to convert it to bitmap. So TBitmap as the last straw calls TPersistent.Assign method and that in turn calls TPngImage.AssignTo and that works like a charm.

Is this side of inheritance needed in your program is up to you. If there is again lots of dublicating code (the one which deals with machines and another with automobiles) or there are lots of conditions, then something is wrong and some good polymorphism might be of help.

Yuriy Afanasenkov
  • 1,440
  • 8
  • 12
  • 1
    Actually, `TPngImage` and `TBitmap` inherit from `TGraphic`. Your example suggests that `TPngImage` inherits from `TBitmap`, which is false. – Jerry Dodge May 26 '17 at 23:51
  • 2
    @JerryDodge I didn't mean that. Just the first example that came to my mind of how TPersistent.Assign is supposed to work. TPngImage and TBitmap could be both descendants of just TPersistent, that would work anyway (if TPngImage author would think that assign to TBitmap is useful thing and implement this case). – Yuriy Afanasenkov May 26 '17 at 23:58
  • 1
    Indeed, I'm sure you didn't mean that. Just pointing it out because it can be misleading to newcomers. But I think it's an even better example to demonstrate how `TGraphic` is the base, while the inherited classes `TBitmap` and `TPngImage` (among others) implement a standard structure in different ways. A `TGraphic` is an abstract class which can be further implemented by inherited classes, which override (and reintroduce) members of its ancestor. – Jerry Dodge May 27 '17 at 00:03