Tuesday, September 14, 2004

Object-to-object casts

In object-oriented code, probably the most common type of cast is between object types.

Up-cast
This cast is not really needed at all, as the compiler will implicitly convert from a descendent class up to a base class. This is conceptually done each time you call an inherited method, pass values into TObject parameters, add objects to a TObjectList and so on. Although the compiler will implicitly handle up-casts, it does allow the programmer to explicitly apply up-casts as well, but they will be optimised away into no-ops:

type 
TParent = class
end;
TChild = class(TParent)
end;
var
Parent: TParent;
Child: TChild;
begin
Child := TChild.Create;
Parent := Child; // Implicit up-cast
Parent := TParent(Child); // Explicit hard-cast
Parent := Child as TParent; // Explicit as-cast
if Child is TParent then // is-test
Writeln('Yup');
end.

The code above demonstrates the syntax used for hard-casts, run-time checked as-casts and the is-check. The Win32 compiler assumes that you are not deliberately breaking the type-system, so it optimises the as-cast into a no-op and the is-test into an Assigned-test.


Down-casts
This means that if you play it dirty and use a hard down-cast to store a TObject in a TChild reference (effectively breaking the type system), the casts and is-check will still succeed;

var 
O: TObject;
// ...
Child := TChild(TObject.Create); // Dirty trick - don't do this
if Child is TParent then // is-test
Writeln('Fake!');
O := Child;
if not (O is TParent) then // is-test
Writeln('Nope!');

The.NET runtime makes it virtually impossible to bypass the type-system (bar unsafe or unmanaged code). While as-casts and is-tests work identically in Win32 and .NET, hard-casts that fail at runtime will return nil in .NET and break the type-system in Win32. So on .NET the code above will actually store nil in Child (because a TObject is not a TChild). A down-cast is casting from a parent-class reference to a child-class reference. This is normally what we mean with object-casts.


Safe vs. unsafe casts
In Win32, hard casts are unsafe, because the compiler will not complain if you perform obviously "illegal" or "impossible" casts. Hard-casts is a way of telling the Win32 compiler;



"Relax, I know what I'm doing. Just close your eyes and reinterpret these bits as the type I'm telling you it is".


So there are no checks and no conversions going on (there are a couple of exceptions as we shall see in a later blog post). In .NET even hard-casts are safe, in the sense that the compiler and runtime will check that the cast is valid. For object-to-object hard-casts, the CLR will check that the source is compatible with the target type - if not, nil is returned instead. Conceptually, in .NET hard-casts like this:

  Target := TTargetClass(Source); 

work like this:

if Source is TTargetClass then
Target := Source
else
Target := nil;

As-casts are safe on both platforms. If the cast does not succeed, an exception will be raised, so:

  Target := Source as TTargetClass;

is conceptually equivalent with

  if Source is TTargetClass then 
Target := Source
else
raise EInvalidCast.Create;

Performance issues
In Win32, there is a certain performance overhead with doing an is- or as-check - the compiler and RTL traverses the inheritance chain linearly to check all the class' parents to search for a match. For most code the overhead is negligible. A common need is to covert a reference to a specific type, but to avoid exceptions in the failing cases, so often you'll see code like this:

if Instance is TMyClass then 
MyObject := Instance as TMyClass;

Experienced Delphi programmers will often balk at this construct, noting that the additional test performed by the as-cast is redundant. Again, for most code the overhead is insignificant, but never-the-less you'll often find it rewritten like this:

if Instance is TMyClass then 
begin
MyObject := TMyClass (Instance);
// ... xxx


Here the safe is-cast is performed, followed by the unsafe hard-cast. But combined with the preceding is-cast, the hard-cast is actually safe! In Win32, only one check of the inheritance chain is performed. In .NET however, the hard-cast performs another is-check (returning nil if it fails, which should be never in the above code). So performance aficionados might be tempted to change the code into this:

  MyObject := TMyClass(Instance);
if Assigned(MyObject) then
begin
// ...

That will work nicely without overhead in .NET, but it will fail (typically crash) in Win32 when the cast is invalid. The examples above gracefully handle the case where the cast fails at runtime. Other times you consider it a programming or configuration error for the cast to fail. This is when you'll typically use the as-cast - it will raise an EInvalidCast exception (alias to System. InvalidCastException in .NET) if it fails. This is fairly common to do in event-handlers, for instance. The generic Sender parameter is TObject, so you might need to type cast it to the actual component type (say TDrawGrid). This is particularly useful when you share a common event handler for multiple components. One way to do this is:

procedure TMyForm.GridsDrawCell(Sender: TObject; ...);
var
Grid: TDrawGrid;
begin
Grid := Sender as TDrawGrid;
// ...
end;

This performs the as-check for each draw-cell operation for all the grids in the form. But the only way for the cast to fail is if you have somehow assigned the event handler to a non-DrawGrid component. Why burden the release version of your application with a check that is bound to succeed every time? An alternative solution is to turn the check into an Assert, like this:

procedure TMyForm.GridsDrawCell(Sender: TObject;  ...); 
var
Grid: TDrawGrid;
begin
Assert(Sender is TDrawGrid);
Grid := TDrawGrid (Sender);
// ...
end;

Now in the release builds, the runtime check will be gone (except on .NET), while silly programmer mistakes will still be caught in the internal debug builds (with assertions enabled). Be careful you're not over-applying this optimisation, however. You do want to keep as-casts or is-tests in code where the runtime type of the object may vary according to external input (user interaction, importing data files, communicating over TCP/IP etc).


The protected-access trick
I've talked about this trick earlier. It basically revolves around performing an invalid hard down-cast to a locally defined class. This is done to get access to the protected parts of the object. For instance:

type 
TControlAccess = class(TControl);
begin
TControlAccess(MyControl).Click;
end;

Click is a protected method of TControl, so normally we cannot call it on a control instance. To fool the type-system, we declare an empty local class that inherits from TControl. Because our code is in the same unit as this new type, we get access to all its protected members. Although MyControl is not really a TControlAccess instance, we cast it into one, to call the Click method. This does works in Win32, but it is really a hack, IMO.


This hack is so common, and many VCL components and Delphi applications rely on it, that Borland felt the need to somehow support this technique even in .NET. It does work in .NET as long as the accessed member resides in the same assembly as the calling code. The compiler marks all protected members as the CLR access level protected-or-assembly. This means that at the IL-level, all protected members are directly accessible to all code in the same assembly.


The Delphi compiler does not allow access to these members unless you perform the casting-trick, however. When it encounters the cast, it simply removes it - there is no trace of it in the IL code. The CLR will allow the code as long as it is intra-assembly. The Delphi compiler will also prevent the call across assemblies, of course. For instance if you try the call above when referencing (and not linking in) the Delphi.Vcl.dll assembly the compiler will emit the following compile time error:


[Error] Cross-assembly protected reference to [Borland.Vcl]TControl.Click in Upcasts.Upcasts

3 comments:

Anonymous said...

Very good article, but you don't talk about class reference and hard-casting.
Why, in Delphi win32, it isn't possible to do this:

type
TMyObj = class(TObject)
end;
TYourObj = class(TMyObj)
end;

TMyObjClass = class of TMyObj;
TYourObjClass = class of TYourObj;

...

var
MyObjRef: TMyObjClass;
YourObjRef: TYourObjClass;
pObj: TMyObj;
dObj: TYourObj;
begin
MyObjRef := TYourObj;
pObj := MyObjRef.Create;
dObj := MyObjRef(pObj); //do not compile
end

Is there a way/hack to hard cast with class reference?
Thanks in advance
Andrea Marani

Hallvard Vassbotn said...

I'm not 100% sure what you try to achieve. but you can't use class references as a casting operator, because the compiler has to compile the cast-code at compile time and the class reference can contain any type (derived) at run-time.

At the time you compile your code, you know the *declared* type of pObj and dObj so to cast from one to the other you just do:

dObj := TYourObj(pObj); // hard-cast

or to make it safer:

dObj := pObj as TYourObj; // safe-cast

No need to involve the class reference in the casting expression.

Did I understand you correctly?

Anonymous said...

Yes, you understand me.
I try to use class references as a casting operator. But I'm not sure that it's impossible to do a "not compile-time casting" in other way.

Thank you very mutch.
Bye
Andrea Marani



Copyright © 2004-2007 by Hallvard Vassbotn