Monday, March 20, 2017

Forward record declaration

Forward declaration is not really a new concept. It was already present in the original Wirth Pascal where it allowed the programmer to do one thing and one thing only – call procedure A from procedure B and procedure B from procedure A. Remember – in ye olde times of good old Pascal we had no interfaces, no classes, no units … just procedures and functions.

Because the code tells more than thousand words, here’s an example (still perfectly valid in modern Object Pascal).

procedure ProcA; forward;

procedure ProcB;
begin
  ProcA;
end;

procedure ProcA;
begin
  ProcB;
end;

A more familiar concept in modern times is forward declaration for classes and interfaces. Again, a code tells more than a long explanation …

type
  TClassA = class;

  TClassB = class
    ObjA: TClassA;
  end;

  TClassA = class
    ObjB: TClassB;
  end;

  IIntfA = interface;

  IIntfB = interface
    function Other: IIntfA;
  end;

  IIntfA = interface
    function Other: IIntfB;
  end;

Object Pascal, however, does not understand the concept of a forward record declaration. IOW, this will not compile.

type
  TRecA = record;

  TRecB = record
    function Other: TRecA;
  end;

  TRecA = record
    function Other: TRecB;
  end;

There’s however a trick that allows us to achieve the same functionality with a different syntactic sugar – record helpers. We can remove the TRecB.Other declaration from TRecB and reintroduce it later via a TRecB helper.

type
  TRecB = record
  end;

  TRecA = record
    function Other: TRecB;
  end;

  TRecBHelper = record helper for TRecB
    function Other: TRecA;
  end;

Just keep in mind that this solution is a bit less stable as other code can hide away the functionality from TRecBHelper by introducing their own helper for TRecB. (A nasty “feature” of the language which really should be fixed loooong ago.)

5 comments:

  1. Nice hack :)

    As an example of other "soft" hacks, I use fake descendants to access protected class methods (only if they are not strict protected).

    Unit A;

    Type
    TSomeClass=class
    protected
    procedure SomeProc;
    end;

    Unit B;

    type
    TFakeDescendant=class(TSomeClass);

    procedure SomeProc;
    var
    SomeClass: TSomeClass;
    begin
    ...
    TFakeDescendant(SomeClass).SomeMethod;

    end;

    ReplyDelete
    Replies
    1. This trick is also used to slightly change behavior of standard components without making full scale design-time package. The new class is defined in the start of PAS file matching standard VCL/FMX name, IDE Form Designer uses standard class, and the real program code uses a new hijacked one.

      Delete
  2. The meaning of data type declaration - is determining SizeOf(type) for one-pass compilers (they said Delphi is no more one-pass compiler, but then it obviously acts as one :-/ ). That provides for forward-declaration of reference-types (classes, interfaces): SizeOf(class-type) == SizeOf(interface-type) == SizeOf(lambda-type) == SizeOf(Pointer).

    This however makes it not possible to do forward-declaration of records: their SIZE should be fixed at the first and only meaningful pass of Delphi compiler.

    Which means you can only add functions, not variables into the record via helpers. And functions within record helpers are just syntactic sugar over good-old static global functions (limiting namespace pollution with regard to Advanced Records operators overloading).

    So, basically, you just reinvented good old "forward procedure declaration", advanced-record style.
    But generalyl records are not about methods but about value cells (member variables) and that is what you can not add late-to-the-party because your "forward declaration" already fixed SizeOf(extensibleForwardRecord)==0;

    I have a biiig bias against using ugly hacks xxx helpers are (Except where ugly hacks are needed for the sake of ugly hacks) and their blatant abuse in XE3+ RTL in particular, and this "invention" of mere overengineered forward procedure declaration just reinforced this bias of mine :-D

    ReplyDelete
    Replies
    1. ...with regard to Advanced Records >> and/or << operators overloading...

      Delete
  3. How about the record forward include a parameter telling how many bytes the record requires. Then when it gets to the actual definition of the record, if it needs a different number of bytes it will raise an exception and inform the programmer what the number of bytes should be. Boom! Problem solved.

    ReplyDelete