Pass the Dog, Get the Cat
July 13, 2020 by Dalija Prasnikar
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.
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:
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.
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.
.
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!!!
[/SHOWTOGROUPS]
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.
Код:
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
Код:
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.
Код:
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.
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.
Код:
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!!!
Код:
procedure TestHack;
var
Dog: TDog;
begin
Dog := nil;
try
MakeHack(Dog);
Dog.Guard;
finally
Dog.Free;
end;
end;