Pass the Dog, Get the Cat
This story begins with the
Why, oh, why can't we use the above constructs? Why does it have to be a compiler error?
Well, the real problem is not in our desired
In the above code, the
Running the
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
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
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 theFreeAndNil
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.
In 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 theMakeHack
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;
What could work would be generic version:
ReplyDeleteFreeAndNil(var Obj: T);
T would be a descendant of TObject, but you would have a copy of FreeAndNil() for each object type in memory that you're using it on.
And I don't know if there is autoboxing for this, or if you would need to call FreeAndNil with the type everytime (FreeAndNil(dog), FreeAndNil(cat), ...)
Delphi currently does not support standalone generic procedures.
DeleteRSP-13724
Right now, closest replacement would be making generic class method and since Delphi Rio type inference would allow omitting type.
See: Alternate FreeAndNil implementation on Stack Overflow
Oh, you already had that idea. :D
DeleteAn blogspot omitted the brackets for the generics...
I didn't know about standalone procedures missing generics support, thanks for the heads-up.
Code with Interface :)
ReplyDelete