Delphi: Understanding constructors
Asked Answered
Q

4

41

i'm looking to understand

  • virtual
  • override
  • overload
  • reintroduce

when applied to object constructors. Every time i randomly add keywords until the compiler shuts up - and (after 12 years of developing with Delphi) i'd rather know what i'm doing, rather than trying things randomly.

Given a hypothetical set of objects:

TComputer = class(TObject)
public
    constructor Create(Cup: Integer); virtual;
end;

TCellPhone = class(TComputer)
public
    constructor Create(Cup: Integer; Teapot: string); virtual;
end;

TiPhone = class(TCellPhone)
public
    constructor Create(Cup: Integer); override;
    constructor Create(Cup: Integer; Teapot: string); override;
end;

The way i want them to behave is probably obvious from the declarations, but:

  • TComputer has the simple constructor, and descendants can override it
  • TCellPhone has an alternate constructor, and descendants can override it
  • TiPhone overrides both constructors, calling the inherited version of each

Now that code doesn't compile. i want to understand why it doesn't work. i also want to understand the proper way to override constructors. Or perhaps you could never override constructors? Or perhaps it is perfectly acceptable to override constructors? Perhaps you should never have multiple constructors, perhaps it is perfectly acceptable to have multiple constructors.

i want to understand the why. Fixing it would then be obvious.

See also

Edit: i'm also looking to get some reasoning on the order of virtual, override, overload, reintroduce. Because when trying all combinations of keywords, the number of combinations explodes:

  • virtual; overload;
  • virtual; override;
  • override; overload;
  • override; virtual;
  • virtual; override; overload;
  • virtual; overload; override;
  • overload; virtual; override;
  • override; virtual; overload;
  • override; overload; virtual;
  • overload; override; virtual;
  • etc

Edit 2: i guess we should begin with "is the object hierarchy given even possible?" If not, why not? For example, is it fundamentally incorrect to have a constructor from an ancestor?

TComputer = class(TObject)
public
    constructor Create(Cup: Integer); virtual;
end;

TCellPhone = class(TComputer)
public
    constructor Create(Cup: Integer; Teapot: string); virtual;
end;

i would expect that TCellPhone now has two constructors. But i can't find the combination of keywords in Delphi to make it think that's a valid thing to do. Am i fundamentally wrong in thinking i can have two constructors here in TCellPhone?


Note: Everything below this line is not strictly needed to answer the question - but it does help to explain my thinking. Perhaps you can see, based on my thought processes, what fundamental piece i'm missing that makes everything clear.

Now these declarations don't compile:

//Method Create hides virtual method of base type TComputer:
TCellPhone = class(TComputer)
   constructor Create(Cup: Integer; Teapot: string);  virtual;

//Method Create hides virtual method of base type TCellPhone:
TiPhone = class(TCellPhone)
public
   constructor Create(Cup: Integer); override;
   constructor Create(Cup: Integer; Teapot: string); overload;  <--------
end;

So first i'll trying fixing TCellPhone. i'll start by randomly adding the overload keyword (i know i don't want reintroduce because that would hide the other constructor, which i don't want):

TCellPhone = class(TComputer)
public
   constructor Create(Cup: Integer; Teapot: string); virtual; overload;
end;

But that fails: Field definition not allowed after methods or properties.

i know from experience that, even though i don't have a field after a method or property, if i reverse the order of the virtual and overload keywords: Delphi will shut up:

TCellPhone = class(TComputer)
public
   constructor Create(Cup: Integer; Teapot: string); overload; virtual; 
end;

But i still get the error:

Method 'Create' hides virtual method of base type 'TComputer'

So i try removing both keywords:

TCellPhone = class(TComputer)
public
   constructor Create(Cup: Integer; Teapot: string);
end;

But i still get the error:

Method 'Create' hides virtual method of base type 'TComputer'

So i resign myself to now trying reintroduce:

TCellPhone = class(TComputer)
public
   constructor Create(Cup: Integer; Teapot: string); reintroduce;
end;

And now TCellPhone compiles, but it has made things much worse for TiPhone:

TiPhone = class(TCellPhone)
public
   constructor Create(Cup: Integer); override; <-----cannot override a static method
   constructor Create(Cup: Integer; Teapot: string); override; <-----cannot override a static method
end;

Both are complaining that i cannot override them, so i remove the override keyword:

TiPhone = class(TCellPhone)
public
   constructor Create(Cup: Integer);
   constructor Create(Cup: Integer; Teapot: string);
end;

But now the 2nd create says it must be marked with overload, which i do (in fact i'll mark both as overload, since i know what will happen if i don't):

TiPhone = class(TCellPhone)
public
   constructor Create(Cup: Integer); overload;
   constructor Create(Cup: Integer; Teapot: string); overload;
end;

All all is good in the interface section. Unfortunately my implementations won't work. My single parameter constructor of TiPhone cannot call the inherited constructor:

constructor TiPhone.Create(Cup: Integer);
begin
    inherited Create(Cup); <---- Not enough actual parameters
end;
Quixotism answered 6/10, 2010 at 19:8 Comment(3)
From Delphi 9 help: Method declarations can include special directives that are not used with other functions or procedures. Directives should appear in the class declaration only, not in the defining declaration, and should always be listed in the following order: reintroduce; overload; binding; calling convention; abstract; warning where binding is virtual, dynamic, or override; calling convention is register, pascal, cdecl, stdcall, or safecall; and warning is platform, deprecated, or library.Saporous
Delphi 5 still came with an excellent printed Language Guide explaining those declarations in details.Fingernail
Try TCellPhone.Create(Cup); instead of inherited Create(Cup);Drilling
E
21

I see two reasons your original set of declarations shouldn't compile cleanly:

  1. There should be a warning in TCellPhone that its constructor hides the method of the base class. This is because the base-class method is virtual, and the compiler worries that you're introducing a new method with the same name without overriding the base-class method. It doesn't matter that the signatures differ. If your intention is indeed to hide the method of the base class, then you need to use reintroduce on the descendant declaration, as one of your blind guesses showed. The sole purpose of that directive is to quell the warning; it has no effect on run-time behavior.

    Ignoring what's going to happen with TIPhone later on, the following TCellPhone declaration is what you'd want. It hides the ancestor method, but you want it to be virtual as well. It won't inherit the virtualness of the ancestor method because they're two completely separate methods that just happen to have the same name. Therefore, you need to use virtual on the new declaration as well.

    TCellPhone = class(TComputer)
    public
      constructor Create(Cup: Integer; Teapot: string); reintroduce; virtual;
    end;
    

    The base-class constructor, TComputer.Create, is also hiding a method of its ancestor, TObject.Create, but since the method in TObject is not virtual, the compiler doesn't warn about it. Hiding non-virtual methods happens all the time and is generally unremarkable.

  2. You should get an error in TIPhone because there is no longer any one-argument constructor to override. You hid it in TCellPhone. Since you want to have two constructors, reintroduce clearly wasn't the right choice to use earlier. You don't want to hide the base-class constructor; you want to augment it with another constructor.

    Since you want both constructors to have the same name, you need to use the overload directive. That directive needs to be used on all the original declarations — the first time each distinct signature is introduced subsequent declarations in descendants. I thought it was required on all declarations (even the base class), and it doesn't hurt to do that, but I guess it's not required. So, your declarations should look like this:

    TComputer = class(TObject)
    public
      constructor Create(Cup: Integer);
        overload; // Allow descendants to add more constructors named Create.
        virtual;  // Allow descendants to re-implement this constructor.
    end;
    
    TCellPhone = class(TComputer)
    public
      constructor Create(Cup: Integer; Teapot: string);
        overload; // Add another method named Create.
        virtual;  // Allow descendants to re-implement this constructor.
    end;
    
    TiPhone = class(TCellPhone)
    public
      constructor Create(Cup: Integer);
        override; // Re-implement the ancestor's Create(Integer).
      constructor Create(Cup: Integer; Teapot: string);
        override; // Re-implement the ancestor's Create(Integer, string).
    end;
    

Modern documentation tells what order everything should go in:

reintroduce; overload; binding; calling convention; abstract; warning

where binding is virtual, dynamic, or override; calling convention is register, pascal, cdecl, stdcall, or safecall; and warning is platform, deprecated, or library.

Those are six different categories, but in my experience, it's rare to have more than three on any declaration. (For example, functions that need calling conventions specified probably aren't methods, so they can't be virtual.) I never remember the order; I've never seen it documented till today. Instead, I think it's more helpful to remember each directive's purpose. When you remember which directives you need for different tasks, you'll end up with just two or three, and then it's pretty simple to experiment to get a valid order. The compiler might accept multiple orders, but don't worry — order isn't important in determining meaning. Any ordering the compiler accepts will have the same meaning as any other (except for calling conventions; if you mention more than one of those, only the last one counts, so don't do that).

So, then you just have to remember the purpose of each directive, and think about which ones don't make any sense together. For example, you cannot use reintroduce and override at the same time because they have opposite meanings. And you can't use virtual and override together because one implies the other.

If you have lots of directives piling up, you can always cut overload out of the picture while you work out the rest of the directives you need. Give your methods different names, figure out which of the other directives they need by themselves, and then add overload back while you give them all the same names again.

Emerald answered 6/10, 2010 at 21:37 Comment(0)
O
6

Note that I don't have Delphi 5, so I'm basing my answers off the newest version, Delphi XE. I don't think that will really make any difference here, but if it does, you've been warned. :)

This is mostly based on http://docwiki.embarcadero.com/RADStudio/en/Methods, which is the current documentation of how methods work. Your Delphi 5 help file probably has something similar to this as well.

First off, a virtual constructor may not make much sense here. There are a few cases where you do want this, but this probably isn't one. Take a look at http://docwiki.embarcadero.com/RADStudio/en/Class_References for a situtation where you do need a virtual constructor - if you always know the type of your objects when coding, however, you don't.

The problem you then get in your 1-parameter constructor is that your parent class doesn't have a 1-parameter constructor itself - inherited constructors are not exposed. You can't use inherited to go up multiple levels in the hierarchy, you can only call your immediate parent. You will need to call the 2-parameter constructor with some default value, or add a 1-parameter constructor to TCellPhone as well.

In general, the four keywords have the following meanings:

  • virtual - Mark this as a function where you will want run-time dispatching (allows polymorphic behavior). This is only for the initial definition, not when overriding in subclasses.
  • override - Provide a new implementation for a virtual method.
  • overload - Mark a function with the same name as another function, but a different parameter list.
  • reintroduce - Tell the compiler you actually intended to hide a virtual method, instead of merely forgetting to supply override.

The ordering required is detailed in the documentation:

Method declarations can include special directives that are not used with other functions or procedures. Directives should appear in the class declaration only, not in the defining declaration, and should always be listed in the following order:

reintroduce; overload; binding; calling convention; abstract; warning

where binding is virtual, dynamic, or override; calling convention is register, pascal, cdecl, stdcall, or safecall; and warning is platform, deprecated, or library.

Onomatopoeia answered 6/10, 2010 at 19:43 Comment(5)
Why does the simpler example, using only the first two classes, still fail? The compiler complains that Create(int, string) hides base Create(int). It has a different signature, how is it hiding it?Quixotism
@Ian, they have the same name. If you want two things with the same name but different signatures, then you are overloading, and both declarations need to use the overload directive.Emerald
@Ian: Also, if you simply don't use virtual the compiler shouldn't emit the warning about hiding anything - the constructors are associated directly with their class.Onomatopoeia
@Rob Kennedy Can you give an example of how you add the overload directive? (Like i said i've tried every random combination of keywords - and i can't make it not complain). The form of another answer would be perfect - allowing readable code.Quixotism
The problem with a constructor not being virtual is that one constructor calling a sibling constructor won't get the overridden implementation in a descendant.Quixotism
S
3

This is a working implementation of the definitions wanted:

program OnConstructors;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type

TComputer = class(TObject)
public
    constructor Create(Cup: Integer); virtual;
end;

TCellPhone = class(TComputer)
public
    constructor Create(Cup: Integer; Teapot: string); reintroduce; overload; virtual;
end;

TiPhone = class(TCellPhone)
public
    constructor Create(Cup: Integer); overload; override;
    constructor Create(Cup: Integer; Teapot: string); override;
end;

{ TComputer }

constructor TComputer.Create(Cup: Integer);
begin
  Writeln('Computer: cup = ', Cup);
end;

{ TCellPhone }

constructor TCellPhone.Create(Cup: Integer; Teapot: string);
begin
  inherited Create(Cup);
  Writeln('Cellphone: teapot = ', Teapot);
end;

{ TiPhone }

constructor TiPhone.Create(Cup: Integer);
begin
  inherited Create(Cup);
  Writeln('iPhone: cup = ', Cup);
end;

constructor TiPhone.Create(Cup: Integer; Teapot: string);
begin
  inherited;
  Writeln('iPhone: teapot = ', Teapot);
end;

var
  C: TComputer;

begin

  C := TComputer.Create(1);
  Writeln; FreeAndNil(C);

  C := TCellPhone.Create(2);
  Writeln; FreeAndNil(C);
  C := TCellPhone.Create(3, 'kettle');
  Writeln; FreeAndNil(C);

  C := TiPhone.Create(4);
  Writeln; FreeAndNil(C);
  C := TiPhone.Create(5, 'iPot');

  Readln; FreeAndNil(C);

  end.

with results:

Computer: cup = 1

Computer: cup = 2

Computer: cup = 3
Cellphone: teapot = kettle

Computer: cup = 4
iPhone: cup = 4

Computer: cup = 5
Cellphone: teapot = iPot
iPhone: teapot = iPot

The first part is in accordance with this. The definition of the TiPhone two constructors then proceeds as follows:

  • The first constructor is overloading one of the two constructors inherited and overriding its sibling. To achieve this, use overload; override to overload the TCellPhone one while overriding the other constructor.
  • That being done, the second constructor needs a simple override to override its sibling.
Saporous answered 6/10, 2010 at 22:46 Comment(0)
T
0

use overload on both, it's the way i do it, and it works.

constructor Create; Overload; <-- use overload here

constructor Values; Overload; <-- and here

remember not to use the same name for two different constructors

Tuque answered 19/8, 2016 at 20:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.