Pass the Dog, Get the Cat

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.

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 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;

Comments

  1. What could work would be generic version:

    FreeAndNil(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), ...)

    ReplyDelete
    Replies
    1. Delphi currently does not support standalone generic procedures.
      RSP-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

      Delete
    2. Oh, you already had that idea. :D
      An blogspot omitted the brackets for the generics...

      I didn't know about standalone procedures missing generics support, thanks for the heads-up.

      Delete

Post a Comment

Popular posts from this blog

Delphi 12.1 & New Quality Portal Released

Assigning result to a function from asynchronous code

Coming in Delphi 12: Disabled Floating-Point Exceptions