Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to cast between compatible method pointers?

I have two different method pointers.

type
  TComponentMethod = procedure(const AComponent: TComponent) of object;
  TFormMethod = procedure(const AForm: TForm) of object;

The only difference is the type of the argument, but both are object references so it should not make any difference from the calling convention point of view.

(However it could be a type safety problem, because of co- / contravariance.)

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
  private
    procedure M2(const AForm: TForm);
  end;

procedure TForm1.FormCreate(Sender: TObject);
var
  FormMethod: TFormMethod;
  ComponentMethod: TComponentMethod;
begin
  FormMethod := M2;
  // How to cast this?
  ComponentMethod := M2;
end;

The compiler doesn't let me do it.

[dcc32 Error] Unit1.pas(32): E2010 Incompatible types: 'TComponent' and 'TForm'

Is there any way to type cast a method pointer to another "compatible" method pointer?

like image 576
Jens Mühlenhoff Avatar asked Mar 15 '26 17:03

Jens Mühlenhoff


2 Answers

You can do this:

var
  FormMethod: TFormMethod;
  ComponentMethod: TComponentMethod;
begin
  FormMethod := M2;
  ComponentMethod := TComponentMethod(FormMethod);
end;

As far as I can see, the trick is that you need to assign to a temporary local variable before subsequently assigning that to ComponentMethod.

As you know, this is not typesafe. If ComponentMethod is invoked with an argument that is not derived from TForm, then the compiler won't be able to save you.

like image 126
David Heffernan Avatar answered Mar 18 '26 10:03

David Heffernan


They're not compatible.

If M2 takes a TForm as one of its inputs, the compiler can reasonably expect form methods/members to be accessed in the body of M2.

A TComponentMethod event handler only requires a TComponent instance to call it. So the combination (if the compiler allowed it) would make it possible to access TForm members on TComponent instances. (Obviously a recipe for disaster.)

E.g.

procedure TForm1.M2(const AForm: TForm);
begin
  AForm.ModalResult := mrCancel;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  ComponentMethod: TComponentMethod;
begin
  ComponentMethod := M2;

  //The next line is legal. But if the previous line were legal,
  //you'd attempt to access TForm(AComponent).ModalResult ...
  //An AV if you're lucky, and weird behaviour if you're not.
  ComponentMethod(TComponent.Create(Self));
end;

That said, you can perform a hard-typecast. However, TComponentMethod(M2) wouldn't work because the compiler wants to call M2 almost any time you use the identifier. It's only a bit of "compiler magic" that that allows FormMethod := M2 in the first place. So you'd need an intermediate event handler variable to hold a reference to M2. And because an event handler variable is not a function, it can be typecast without trying to call it.

Temp := M2;
ComponentMethod := TComponentMethod(Temp);

WARNING This is a terrible idea, but it works:

  • Reminder: The ComponentMethod variable can be used as follows: ComponentMethod(AComponentThatsNotAForm).
  • If you can truly guarantee that the previous call is safe because of how M2 is implemented, then you should have declared M2 as follows: procedure M2(const AComponent: TComponent); (If it doesn't need a form instance to do its job, don't ask for a form instance.) ... And you wouldn't have to bother trying so desperately to find a way to shoot yourself in the foot.
like image 21
Disillusioned Avatar answered Mar 18 '26 10:03

Disillusioned