Virtual methods in Delphi
July 12, 2021 by Dalija Prasnikar
Virtual methods enable subtype polymorphism - in other words, they allow implementing different behavior in descendant classes.
That means if you have a base TShape class with a virtual Paint method, and several descendant classes like TRectangle, TCircle and TTriangle, then each of those subclasses can implement a different Paint method to appropriately paint itself. You can call the Paint method on any shape instance without needing to know which kind it is, and it will be correctly painted.
Static dispatching means that the compiler will resolve the method address at compile time, and will emit code that will directly call that address without any indirections.
Dynamic dispatching is a bit more complicated. Instead of pointing to the method address directly, the compiler will point to a particular slot (address) in the VMT— virtual method table—associated with every object instance. Depending on the exact class of the instance, that VMT table will hold different addresses pointing to the last method override in the class hierarchy.
TShape = class
protected
FX, FY: Integer;
public
procedure Paint;
procedure Move(AX, AY: Integer);
property X: Integer read FX write FX;
property Y: Integer read FY write FY;
end;
procedure TShape.Paint;
begin
Writeln('Painting shape at');
Writeln('X: ', X);
Writeln('Y: ', Y);
end;
procedure TShape.Move(AX, AY: Integer);
begin
FX := AX;
FY := AY;
Paint;
end;
var
Shape: TShape;
begin
Shape := TShape.Create;
Shape.Move(10, 20);
Shape.Paint;
...
end;
When we call Shape.Move(10, 20), the compiler roughly translates it to Move(Shape, 10, 20). When we call Shape.Paint, it is translated to Paint(Shape).
Calling another class method from within the method implementation will pass Self as a parameter. For instance:
procedure TShape.Move(AX, AY: Integer);
begin
FX := AX;
FY := AY;
Paint; // this will translate to Paint(Self)
end;
This hidden object instance parameter is also crucial for understanding the differences between static and dynamic dispatching, as well as some special tricks you can use with statically bound methods.
TShape = class
public
procedure Paint;
end;
The Paint method in the above declaration is statically bound. When the compiler encounters a call to Shape.Paint, it will resolve it directly to the TShape.Paint address. If we translate further by adding an object instance parameter, our call will look like TShape.Paint(Shape).
This will be equivalent to having a standalone Paint procedure with one parameter of the TShape type:
TShape = class
protected
FX, FY: Integer;
public
property X: Integer read FX write FX;
property Y: Integer read FY write FY;
end;
procedure ShapePaint(AShape: TShape);
begin
Writeln('Painting shape at');
Writeln('X: ', AShape.X);
Writeln('Y: ', AShape.Y);
end;
procedure ShapeMove(AShape: TShape; AX, AY: Integer);
begin
AShape.X := AX;
AShape.Y := AY;
ShapePaint(AShape);
end;
var
Shape: TShape;
begin
Shape := TShape.Create;
// this is equivalent of calling TShape.Paint in previous declaration
ShapePaint(Shape);
...
Since TShape.Paint is resolved at compile time, and the method resolution (address) itself does not involve a particular object instance—it will only be passed as a parameter— we can call Paint on a nil object reference, just like we can pass nil to a standalone procedure. Of course, if some of our code inside the procedure tries to access any data or other code inside that nil instance, we will have an access violation exception at that point, but the call to the method itself will never cause any crashes.
If we check whether the passed object instance is nil, and perform work only if it is not nil, then we will have perfectly working code and no crashes:
procedure TShape.Paint;
begin
if Assigned(Self) then
begin
Writeln('Painting shape at');
Writeln('X: ', X);
Writeln('Y: ', Y);
end;
end;
procedure ShapePaint(AShape: TShape);
begin
if Assigned(AShape) then
begin
Writeln('Painting shape at');
Writeln('X: ', AShape.X);
Writeln('Y: ', AShape.Y);
end;
end;
For the most of the code it will make no sense to check whether the instance inside a statically bound method is nil or not, but the fact that we can safely call statically bound methods on nil references allows us to implement tricks in special methods like Free, where in real-life code it is possible to have situation where a method could be called on a nil reference and checking for nil from the outside would result in excessive checking everywhere.
Note: Checking Self for nil in statically bound methods is appropriate only if nil is a valid argument. It should never be used as a crash prevention measure in cases where you never expect nil, but you want to be safe just in case you have some bug somewhere.
July 12, 2021 by Dalija Prasnikar
Virtual methods enable subtype polymorphism - in other words, they allow implementing different behavior in descendant classes.
That means if you have a base TShape class with a virtual Paint method, and several descendant classes like TRectangle, TCircle and TTriangle, then each of those subclasses can implement a different Paint method to appropriately paint itself. You can call the Paint method on any shape instance without needing to know which kind it is, and it will be correctly painted.
Static vs dynamic dispatch
Virtual methods can achieve polymorphism via a mechanism called dynamic dispatching at runtime, while non-virtual methods will be statically dispatched (bound) during compilation. Polymorphism cannot be achieved by statically bound methods—in other words, when you call a statically bound method, you will call the same implementation for all descendant classes.Static dispatching means that the compiler will resolve the method address at compile time, and will emit code that will directly call that address without any indirections.
Dynamic dispatching is a bit more complicated. Instead of pointing to the method address directly, the compiler will point to a particular slot (address) in the VMT— virtual method table—associated with every object instance. Depending on the exact class of the instance, that VMT table will hold different addresses pointing to the last method override in the class hierarchy.
Method calls
Methods are similar to regular functions and procedures with one significant difference—they also pass an additional (hidden) parameter, identifying the object instance upon which they were called:TShape = class
protected
FX, FY: Integer;
public
procedure Paint;
procedure Move(AX, AY: Integer);
property X: Integer read FX write FX;
property Y: Integer read FY write FY;
end;
procedure TShape.Paint;
begin
Writeln('Painting shape at');
Writeln('X: ', X);
Writeln('Y: ', Y);
end;
procedure TShape.Move(AX, AY: Integer);
begin
FX := AX;
FY := AY;
Paint;
end;
var
Shape: TShape;
begin
Shape := TShape.Create;
Shape.Move(10, 20);
Shape.Paint;
...
end;
When we call Shape.Move(10, 20), the compiler roughly translates it to Move(Shape, 10, 20). When we call Shape.Paint, it is translated to Paint(Shape).
Calling another class method from within the method implementation will pass Self as a parameter. For instance:
procedure TShape.Move(AX, AY: Integer);
begin
FX := AX;
FY := AY;
Paint; // this will translate to Paint(Self)
end;
This hidden object instance parameter is also crucial for understanding the differences between static and dynamic dispatching, as well as some special tricks you can use with statically bound methods.
Static dispatch
In Delphi, methods are statically bound by default. Only if they are marked with the virtual, dynamic or override directives will they be dynamically bound:TShape = class
public
procedure Paint;
end;
The Paint method in the above declaration is statically bound. When the compiler encounters a call to Shape.Paint, it will resolve it directly to the TShape.Paint address. If we translate further by adding an object instance parameter, our call will look like TShape.Paint(Shape).
This will be equivalent to having a standalone Paint procedure with one parameter of the TShape type:
TShape = class
protected
FX, FY: Integer;
public
property X: Integer read FX write FX;
property Y: Integer read FY write FY;
end;
procedure ShapePaint(AShape: TShape);
begin
Writeln('Painting shape at');
Writeln('X: ', AShape.X);
Writeln('Y: ', AShape.Y);
end;
procedure ShapeMove(AShape: TShape; AX, AY: Integer);
begin
AShape.X := AX;
AShape.Y := AY;
ShapePaint(AShape);
end;
var
Shape: TShape;
begin
Shape := TShape.Create;
// this is equivalent of calling TShape.Paint in previous declaration
ShapePaint(Shape);
...
Since TShape.Paint is resolved at compile time, and the method resolution (address) itself does not involve a particular object instance—it will only be passed as a parameter— we can call Paint on a nil object reference, just like we can pass nil to a standalone procedure. Of course, if some of our code inside the procedure tries to access any data or other code inside that nil instance, we will have an access violation exception at that point, but the call to the method itself will never cause any crashes.
If we check whether the passed object instance is nil, and perform work only if it is not nil, then we will have perfectly working code and no crashes:
procedure TShape.Paint;
begin
if Assigned(Self) then
begin
Writeln('Painting shape at');
Writeln('X: ', X);
Writeln('Y: ', Y);
end;
end;
procedure ShapePaint(AShape: TShape);
begin
if Assigned(AShape) then
begin
Writeln('Painting shape at');
Writeln('X: ', AShape.X);
Writeln('Y: ', AShape.Y);
end;
end;
For the most of the code it will make no sense to check whether the instance inside a statically bound method is nil or not, but the fact that we can safely call statically bound methods on nil references allows us to implement tricks in special methods like Free, where in real-life code it is possible to have situation where a method could be called on a nil reference and checking for nil from the outside would result in excessive checking everywhere.
Note: Checking Self for nil in statically bound methods is appropriate only if nil is a valid argument. It should never be used as a crash prevention measure in cases where you never expect nil, but you want to be safe just in case you have some bug somewhere.