Articles Pass the Dog, Get the Cat

FireWind

Завсегдатай
Staff member
Moderator
Pass the Dog, Get the Cat
July 13, 2020 by Dalija Prasnikar
[SHOWTOGROUPS=4,20]
This story begins with the FreeAndNil procedure, why its signature could not have a typed var parameter, why we can only pass variables declared as TObject to such procedures, and why the compiler refuses to compile if we try passing any other variable type even if it is a descendant of TObject.
Code:
procedure FreeAndNil(var Obj);
So why does the compiler enforce this behavior?
If we take a look at the FreeAndNil implementation and imagine it with a typed var parameter, it may seem that the compiler is throwing us curve balls for nothing. There is nothing we do inside that procedure that would warrant a compiler error for passing TObject descendant types:
  • assigning variable to another TObject variable - pass
  • assigning nil to original variable - pass
  • freeing original object instance stored in temporary variable - pass
Code:
procedure FreeAndNil(var Obj: TObject);
var
  Temp: TObject;
begin
  Temp := TObject(Obj);
  Pointer(Obj) := nil;
  Temp.Free;
end;


var
  Obj: TSomeObject;
...
  FreeAndNil(Obj);
Why, oh, why can't we use the above constructs? Why does it have to be a compiler error?

E2033 Types of actual and formal var parameters must be identical

Well, the real problem is not in our desired FreeAndNil implementation, but in some other really dangerous code we might write all over the place if the compiler would let us. Nilling the variable is safe, but that is all the safety we can get. Constructing a new object instance and returning it could wreak havoc. We could end up with variables containing object instances of incompatible types and operating on those instances using non-existent methods, accessing non-existent fields, causing wrong behavior and memory corruption all over the place.

Code:
program DogCat;

{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.Classes;

type
  TAnimal = class
  public
  end;

  TDog = class(TAnimal)
  private
    IsBarking: boolean;
  public
    procedure Bark;
    procedure Stop;
    procedure Guard; virtual;
  end;

  TCat = class(TAnimal)
  private
    Name: string;
  public
  end;

procedure TDog.Bark;
begin
  IsBarking := true;
  Writeln('Dog is barking');
end;

procedure TDog.Stop;
begin
  IsBarking := false;
  Writeln('Dog is not barking');
end;

procedure TDog.Guard;
begin
  Writeln('Dog is guarding');
end;

procedure Make(var Animal: TAnimal);
begin
  Animal := TCat.Create;
  Writeln('Cat created');
end;

procedure MakeHack(var Animal);
begin
  TAnimal(Animal) := TCat.Create;
  Writeln('Cat created');
end;

procedure Test;
var
  Dog: TAnimal;
begin
  Dog := nil;
  try
    Make(Dog);
    // since Dog is TAnimal, we have to use type casting in order to call Bark
    // if the Dog variable contains anything that is not TDog or its descendant
    // the typecast will cause a runtime exception, but before we get the chance to corrupt memory
    // This code might break but it will at least break in a controllable manner
    (Dog as TDog).Bark;
  finally
    Dog.Free;
  end;
end;

procedure TestHack;
var
  Dog: TDog;
begin
  Dog := nil;
  try
    MakeHack(Dog);
    Dog.Bark;
//    Dog.Stop;
  finally
    Dog.Free;
  end;
end;

begin
  try
    // Test;
    TestHack;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;

  Readln;
end.
n the above code, the MakeHack procedure allows us to simulate what would actually happen if the compiler would not enforce the type of a var parameter.

Running the TestHack procedure throws an exception.

What happened?
Well, we passed a Dog and got a Cat. Calling methods that operate on Dog corrupted the string field in the Cat object instance.

Getting the exception is good, you might say. The compiler should allow us to write such code anyway and having the exception would warn us we did something wrong. We could easily fix that code.

Well, not so much. If you uncomment the Dog.Stop line in TestHack method, there will be no exception. But, no exception does not mean there is no problem. Dog.Bark is corrupting memory. Period. In a more complex scenario, that corruption could lead to serious misbehavior and tracking down such issues is extremely hard if the exception does not happen at the call site.

Still not convinced the compiler is doing a good job here?
Let's change the MakeHack implementation to something more belivable. Squeezing the Cat into an Animal was obviously wrong.
Code:
procedure MakeHack(var Animal);
begin
  TAnimal(Animal) := TAnimal.Create;
  Writeln('Animal created');
end;
.
Now, that is how proper code should look like. Dog is an animal, and there is no more pass the Dog, get the Cat situation.

If you think that solves the problem, you got it wrong. You can still corrupt memory that does not belong to you and I dare you to call the Guard method on such Dog instance. Kaboommm!!!
Code:
procedure TestHack;
var
  Dog: TDog;
begin
  Dog := nil;
  try
    MakeHack(Dog);
    Dog.Guard;
  finally
    Dog.Free;
  end;
end;
[/SHOWTOGROUPS]
 
Top