11

I need a clarification on Delphi Type Casting
I wrote an example with two classes: TClassA and TClassB, TClassB derived from TClassA.

the code is as follows:

program TEST;

{$APPTYPE CONSOLE}


uses
  System.SysUtils;

type
  TClassA = class(TObject)
  public
    Member1:Integer;
    constructor Create();
    function ToString():String; override;
  end;

type
  TClassB = class(TClassA)
  public
    Member2:Integer;
    constructor Create();
    function ToString():String; override;
    function MyToString():String;
  end;

{ TClassA }

constructor TClassA.Create;
begin
  Member1 := 0;
end;

function TClassA.ToString: String;
begin
  Result := IntToStr(Member1);
end;

{ TClassB }

constructor TClassB.Create;
begin
  Member1 := 0;
  Member2 := 10;
end;

function TClassB.MyToString: String;
begin
  Result := Format('My Values is: %u AND %u',[Member1,Member2]);
end;

function TClassB.ToString: String;
begin
  Result := IntToStr(Member1) + ' - ' + IntToStr(Member2);
end;


procedure ShowInstances();
var
  a: TClassA;
  b: TClassB;
begin
  a := TClassA.Create;
  b := TClassB(a); // Casting (B and A point to the same Memory Address)
  b.Member1 := 5;
  b.Member2 := 150; // why no error? (1)

  Writeln(Format('ToString: a = %s, a = %s',[a.ToString,b.ToString])); // (2)
  Writeln(Format('Class Name: a=%s, b=%s',[a.ClassName,b.ClassName])); // (3)
  Writeln(Format('Address: a=%p, b=%p',[@a,@b])); // (4)
  Writeln(b.MyToString); // why no error? (5)

  readln;
end;

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

The program output is:

ToString: a = 5, a = 5
Class Name: a=TClassA, b=TClassA
Address: a=0012FF44, b=0012FF40
My Values is: 5 AND 150

(1) what's the Member2 address? is this a possible "Access Violation"?
(2) ok, ToString() method point to the same address
(3) why a and b have the same ClassName?
(4) ok, a and b are two different variables
(5) if b is TClassA, why can use "MyToString" method?

AndreaBoc
  • 3,077
  • 2
  • 17
  • 22

3 Answers3

30

You're applying a hard type cast over the variable. When you do so, you're telling the compiler you know what you're doing and the compiler trusts you.

(1) what's the Member2 address? is this a possible "Access Violation"?

When you assign a value to a member of the class, the class definition of the variable is used by the compiler to calculate the offset of that member in the memory space, so when you have a class declaration like this:

type
  TMyClass = class(TObject)
    Member1: Integer; //4 bytes
    Member2: Integer; //4 bytes
  end;

the in-memory representation of this object looks like this:

reference (Pointer) to the object
|
|
--------> [VMT][Member1][Member 2][Monitor]
Offset     0    4        8         12

When you issue a statement like this:

MyObject.Member2 := 20;

The compiler just uses that information to calculate the memory address to apply that assignment to. In this case, the compiler the assignment may be translated to

PInteger(NativeUInt(MyObject) + 8)^ := 20;

So, your assignment succeeds just because the way the (default) memory manager works. An AV is originated by the operating system when you try to access a memory address that is not part of your program. In this case, your program has taken more memory from the OS than required. IMHO, when you don't get an AV, you, in fact, are unlucky, because your program memory may now be silently corrupted. Any other variable that happens to reside at that address may have changed its value (or meta-data), and it would result in undefined behavior.

(2) ToString() method point to the same address

Since the ToString() method is a virtual one, the address of that method is stored in the VMT and the call is determined at runtime. Take a look at What data does a TObject contain?, and read the referenced book chapter: The Delphi Object Model.

(3) why a and b have the same ClassName?

The class name is also part of the run-time metadata of the object. The fact that you're applying the wrong mold to the object doesn't change the object itself.

(4) a and b are two different variables

Of course, you declared it, look at your code:

var
  a: TClassA;
  b: TClassB;

Well, two different variables. In Delphi, the object variables are references, so, after some lines of code both reference the same address, but that's a different thing.

(5) if b is TClassA, why can use "MyToString" method?

Because you're telling the compiler that's OK, and as said, the compiler trusts you. It is hacky, but Delphi is also a low level language and you're allowed to do a lot of crazy things, if you want, but:

Play safe

If you want (and you surely want most of the time) to be on the safe side, don't apply a hard cast like that in your code. Use the as operator:

The as operator performs checked typecasts. The expression

object as class

returns a reference to the same object as object, but with the type given by class. At runtime, object must be an instance of the class denoted by class or one of its descendants, or be nil; otherwise an exception is raised. If the declared type of object is unrelated to class - that is, if the types are distinct and one is not an ancestor of the other - a compilation error results.

So, with the as operator, you're safe, both at compile-time and at run-time.

Change your code to:

procedure ShowInstance(A: TClassA);
var
  b: TClassB;
begin
  b := A as TClassB; //runtime exception, the rest of the compiled code 
                     //won't be executed if a is not TClassB
  b.Member1 := 5;
  b.Member2 := 150; 

  Writeln(Format('ToString: a = %s, a = %s',[a.ToString,b.ToString])); 
  Writeln(Format('Class Name: a=%s, b=%s',[a.ClassName,b.ClassName])); 
  Writeln(Format('Address: a=%p, b=%p',[@a,@b])); 
  Writeln(b.MyToString); 

  readln;
end;

procedure ShowInstances();
begin
  ShowInstance(TClassB.Create); //success
  ShowInstance(TClassA.Create); //runtime failure, no memory corrupted.
end;
jachguate
  • 16,976
  • 3
  • 57
  • 98
  • FWIW, the memory layout is a little different, these days: Member1 is at offset 8 and at offset 4 there is a Monitor field. – Rudy Velthuis Jan 31 '13 at 15:56
  • @Rudy, the Monitor field is at the end of the object, not at the beginning, as shown in the answer. – jachguate Jan 31 '13 at 16:03
  • Yes, you are right. It still beats me why they do it that way, but hey, it is their problem. – Rudy Velthuis Feb 01 '13 at 20:15
  • 1
    "when you DON'T get an AV, you in fact are unlucky" - Nicely said! Indeed AV is your friend! Don't hate them. Love them (and fix them). – Gabriel Jun 24 '19 at 13:42
4
  1. Member2 has an address that was not allocated by a memory manager. The possible result of writing to Member2 is heap corruption with subsequent access violations in a totally different part of a program. This is a very nasty bug and compiler can't help you here. You must know what are you doing when making unsafe type casting.

  2. That is because ToString method is virtual, so its address is determined by the actual type of the class instance being created. If you replace virtual method by static (in your case by replacing override directive by reintroduce) the result will be different.

  3. Because ClassName method is kinda virtual too (not really a member of VMT, but that is insignificant implementation detail).

  4. Yes, a and b are two references to the same instance.

  5. Because ToMyString method is static. The actual type of the instance does not matter for static methods.

kludg
  • 27,213
  • 5
  • 67
  • 118
  • 1
    Actually there is no heap corruption because in this case he is writing into the TMonitor field which is the last field in an instance. Try calling TMonitor.Enter(b) which results in an AV (Read of address 000000A6) for proof. – Stefan Glienke Jan 31 '13 at 14:04
  • @StefanGlienke - That is true for Delphi 2009+. AFAIK `Monitor` field was introduced in Delphi 2009. – kludg Jan 31 '13 at 14:08
  • D2009+ or not, that's heap corruption. You expect the address of a TMonitor to exist at a location on the heap, but it's overwritten. – Sertac Akyuz Jan 31 '13 at 15:59
  • @SertacAkyuz - no, `Monitor` is now the last pointer-size field of any object. So in this particular case heap is not corrupted - `Monitor` field is corrupted instead. – kludg Jan 31 '13 at 16:21
  • @Serg - Isn't this monitor field on the heap? – Sertac Akyuz Jan 31 '13 at 16:26
  • @SertacAkyuz - It is a part of the object instance. It is allocated with object instance. – kludg Jan 31 '13 at 16:30
  • @Serg - My point was, since object instance memory is allocated from the heap, the monitor field also lives in the heap. But nevermind, I failed to make my point.. – Sertac Akyuz Feb 01 '13 at 20:33
  • 1
    @SertacAkyuz - I got you. It is heap corruption, but it affects only this particular instance. :) – kludg Feb 02 '13 at 06:16
3

(1) what's the Member2 address? is this a possible "Access Violation"?

Yes. AV is possible. In your case you have luck :)

ok, ToString() method point to the same address

Yes as VTable is concerned at creation time.

(3) why a and b have the same ClassName?

same answer as in (2).

(4) ok, a and b are two different variables

Not really. you printed address from stack :)

(5) if b is TClassA, why can use "MyToString" method?

b is TClassB but by mistake points to TClassA instance.

You should use as operator for such casts. In this case it would fail.