5

MCVE:

The following code does not compile with error when switching the parameter type from const to var or out in the overloaded method Train of the class TAnimalTrainer

but it compiles if non is specified.

[dcc32 Error] Project14.dpr(41): E2250 There is no overloaded version of 'Train' that can be called with these arguments

program Project14;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type    
  TAnimal = class
  private
  FName: string;   
  end;

  TDog = class(TAnimal)
  public
    constructor Create(Name: string);
  end;

  TAnimalTrainer = record // class or record
  public
    procedure Train({const}var aA: TAnimal); overload; // class method or not
    procedure Train(const aName: string); overload;
  end;

{ TAnimalTrainer }

procedure TAnimalTrainer.Train(const aName: string);
var
  Dog: TDog;
begin
  Dog := nil;
  try
    Dog := TDog.Create(aName);  
    Train(Dog); // error here
  finally
    Dog.Free;
  end;
end;

procedure TAnimalTrainer.Train(var aA: TAnimal);
begin
  aA := nil;
end;

{ TDog }

constructor TDog.Create(Name: string);
begin
  FName := Name;
end;



begin
  try       
    { TODO -oUser -cConsole Main : Insert code here }
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Found workarounds:

  • Omit the var.
  • Cast the local variable to TAnimal(Dog)
  • stick with const.

Question: Is this a bug in the compiler?

Nasreddine Galfout
  • 2,550
  • 2
  • 18
  • 36
  • 1
    Overloads are picky sometimes, you can cast it to an animal: Train(TAnimal(Dog)); – Sertac Akyuz Mar 22 '19 at 22:20
  • @SertacAkyuz or declare it as TAnimal – Nasreddine Galfout Mar 22 '19 at 22:21
  • @SertacAkyuz Say no more I just understood the mistake I made in that comment – Nasreddine Galfout Mar 22 '19 at 22:22
  • 4
    That actually works. You can declare Dog as a TAnimal, even if it contains a TDog instance. The main question is: do you want to change the instance of the animal, so for instance you put in a dog, train it, and get back a cat? Or nil, as in your code.. "Sorry mam, I tried to train your dog, but it died. Here is nil in return") If you don't want that, remove `var`, stick with `const` (or not), and you'll be fine. You can then still change the properties of the animal you train, but not the animal itself... – GolezTrol Mar 22 '19 at 23:09
  • If you do want to be able to actually change it do a new instance or nil, you have to keep `var`, but that limits you in the type of variable you can use. – GolezTrol Mar 22 '19 at 23:11
  • @GolezTrol I thought When declaring Dog as a TAnimal then calling Free on Dog it would call the destructor of TAnimal instead but I was wrong, it does work – Nasreddine Galfout Mar 22 '19 at 23:22
  • 1
    That depends on if what you call is virtual and the class overrides it. ... In the end, this is a bug. – Sertac Akyuz Mar 22 '19 at 23:24
  • 2
    @NasreddineGalfout calling `Free` on a base class pointer is fine, since `TObject.Destroy` is virtual. This is a key requirement of polymorphism to allow derived destructors to be called properly. – Remy Lebeau Mar 23 '19 at 01:22
  • @Remy I think I need to read more about the subject my delphi knowledge is getting buggy :) – Nasreddine Galfout Mar 23 '19 at 08:45
  • @SertacAkyuz Regarding your first comment, that would allow the program to convert a dog into a cat – David Heffernan Mar 23 '19 at 10:54
  • @GolezTrol I read your first comment again and it is gold. – Nasreddine Galfout Mar 23 '19 at 11:15
  • @David - That's correct, my comment was intended to workaround a compiler bug, to convert a dog to an animal - which it already is. – Sertac Akyuz Mar 23 '19 at 11:40
  • @SertacAkyuz There is no compiler bug surfaced in this question – David Heffernan Mar 23 '19 at 12:06
  • @David - There's no question that there is a compiler bug surfaced in this question. You suggest the case without the **var** modifier is a defect, because it compiles. I suggest the other way around. – Sertac Akyuz Mar 23 '19 at 12:39
  • @SertacAkyuz No. I do not suggest that there is any defect at all. The compiler's behaviour is correct for value param, var param and const param. – David Heffernan Mar 23 '19 at 12:43
  • BTW, I'd have no problem with putting in a dog and getting back a trained cat. If that's what it takes to have a trained cat, then be it. – Sertac Akyuz Mar 23 '19 at 13:22
  • @SertacAkyuz but in that case you would end up with cat in dog variable and those two are not compatible. – Dalija Prasnikar Mar 23 '19 at 21:59

2 Answers2

8

Is this a bug in the compiler?

No it is not.

Although you have discovered this in the context of an overloaded method, the overloading is disguising the real problem. It will be much easier to understand the issue if we remove the overload.

So, to that end, consider this program:

type
  TAnimal = class
  end;

  TDog = class(TAnimal)
  end;

procedure GetAnimal(var AAnimal: TAnimal);
begin
  AAnimal := TAnimal.Create;
end;

var
  Dog: TDog;

begin
  GetAnimal(Dog);
end.

This fails to compile in the call to GetAnimal with this error:

[dcc32 Error]: E2033 Types of actual and formal var parameters must be identical

Why does the compiler reject this? Well, imagine if it accepted this. If it did so then when GetAnimal returned the Dog variable would refer to an object that was not a TDog.

To see this, change the body of the program to look like this:

GetAnimal(TAnimal(Dog));
Writeln(Dog.InheritsFrom(TDog));

When you do so, the program compiles, but the output is

FALSE

In the context of your program, the compiler is faced with some overloads. As we have seen in this example, the compiler cannot accept passing a TDog variable to a TAnimal var parameter, so it rejects that overload. It knows that it cannot pass a TDog variable to a string parameter, so that is rejected. At which point, there are no overloaded methods left, hence the error message.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • one more thing why the compiler accepts it if I remove the 'var', I mean I can still do the above. (in my mind when adding 'var' the compiler will not create a second pointer to the first pointer to Dog, I think I read this in one of your answers) – Nasreddine Galfout Mar 23 '19 at 11:07
  • Because the caller can't see the modification – David Heffernan Mar 23 '19 at 11:32
  • I don't understand why the example you give is relevant, the code in the question tries to pass a dog as an animal. If you test if a dog **is** an animal, it is. ... You would probably argue then, the behavior when you don't use the **var** is a compiler defect, you shouldn't be able to pass a dog as an animal... – Sertac Akyuz Mar 23 '19 at 11:42
  • A proper example would be a function that returns a dog and a call site that requests an animal. – Sertac Akyuz Mar 23 '19 at 11:56
  • @SertacAkyuz The example is relevant because it matches the code in the question. In the question the variable passed to the `var` param is of the derived type, and the param is of the base type. As is the case in my question. The example you propose is a different situation. – David Heffernan Mar 23 '19 at 12:06
  • No, you're trying to fit an animal in a dog. The code in the question tries to fit a dog in an animal. – Sertac Akyuz Mar 23 '19 at 12:40
  • @Sertac Look closely at the declaration of the function, and the type of the variable that the user is passing. The compiler rejects those types for exactly the reason I give in the answer. Remember that the compiler rejects this at compile time based on *compile* time types. – David Heffernan Mar 23 '19 at 12:42
  • - *"The compiler rejects those types for exactly the reason I give in the answer."* - Then the reverse should compile, no? E.g.: `procedure GetDog(var ADog: TDog);` - `GetDog(AnAnimal)` . That still fails. – Sertac Akyuz Mar 23 '19 at 13:18
  • 1
    @Sertac that fails because an animal can be passed **in**. The example in the question fails because an animal can be passed **out**. – David Heffernan Mar 23 '19 at 14:16
  • Ok, thanks for bearing with me. I'm thinking one proof that this is an intentional decision can be the implementation of FreeAndNil. I mean, who wants to write code that accepts an untyped parameter instead of fixing the compiler... – Sertac Akyuz Mar 24 '19 at 00:24
  • ... which also illustrates how you sacrifice type safety in order to preserve it... – Sertac Akyuz Mar 24 '19 at 05:03
3

Basic issue with non matching var parameters is the possibility that you end up with wrong type inside the calling variable.

You can trick the compiler to do that by using absolute keyword - that allows you to declare variables of different type that share same space - and simulate what would happen if compiler would allow you to use such construct.

Consider following example

uses
  System.SysUtils;

type
  TAnimal = class
  public
    procedure Run; virtual;
  end;

  TDog = class(TAnimal)
  public
    procedure Bark; virtual;
    procedure Fetch; virtual;
  end;

  TCat = class(TAnimal)
  public
    procedure Meow; virtual;
  end;

procedure TAnimal.Run;
begin
  Writeln('Run');
end;

procedure TDog.Bark;
begin
  Writeln('Bark');
end;

procedure TDog.Fetch;
begin
  Writeln('Fetch');
end;

procedure TCat.Meow;
begin
  Writeln('Meow');
end;

procedure Move(const aA: TAnimal);
begin
  aA.Run;
end;

procedure Train(var aA: TAnimal);
begin
  aA := TCat.Create;
end;

var
  Dog: TDog;
  Cat: TAnimal absolute Dog;
begin
  try
    // we cannot use Dog here, because compiler would refuse to compile such code
    // Cat is TAnimal and compiler allows to pass it
    // since Dog and Cat variables share same address space that is
    // equivalent of calling Train(Dog);
    Train(Cat); 

    Move(Cat);
    Dog.Bark;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

If you run above code you will get following output

Run
Meow

Dog and Cat variables share the same address space, so when you call Train(Cat) as the result you will get TCat instance that you can use either through Cat or Dog variable. Basically, you will end up with TCat instance inside TDog variable.

Clearly, when you call Dog.Bark, you should get Bark as output not Meow. Meow is the first method in TCat just like the Bark is the first method in TDog, and when resolving the Bark address through TCat virtual method table it will find Meow method instead. Since both methods have same signature everything is fine if you consider wrong output as being fine.

Now, if you try calling the Dog.Fetch, the application will crash with AV. There are no matching methods at corresponding address in TCat class and you are basically calling some uninitialized place in memory instead of proper method.

That explains why var or out parameter types must match the caller variable type.

As to why you can pass TDog or TCat as TAnimal const or value parameter. Both TDog and TCat inherit from TAnimal and whatewer you can do with TAnimal instance, both TDog and TCat support it. They can override particular behavior, so your cat can run differently than your dog, but whatever you do it is well defined. You cannot end up running some inexistent code.

procedure Move(const aA: TAnimal);
begin
  aA.Run;
  aA.Fetch; // this will fail to compile - there is no Fetch method in TAnimal class
end;

Of course, this does not prevent you to test for particular class and use type casts to call Fetch if TAnimal actually is a TDog.

procedure Move(const aA: TAnimal);
begin
  aA.Run;
  if aA is TDog then TDog(aA).Fetch;
end;

However, if you abuse typecasting and typecast without checking whether particular variable is actually a TDog instance, you will again trip into AV.

procedure Move(const aA: TAnimal);
begin
  aA.Run;
  TDog(aA).Fetch;
end;
Dalija Prasnikar
  • 27,212
  • 44
  • 82
  • 159
  • Great answer, I should ask more questions like this, you guys are gold when you start talking about pets and how to train them. big fan of the book by the way. – Nasreddine Galfout Mar 23 '19 at 23:05