I am trying to step up my Delphi knowledge so I recently purchased the complete Blaise Pascal Magazine library. I was reading an article from BPM #3 by Primož Gabrijelčič called "Introduction to Enumerators". (See attached image)


I am trying to step up my Delphi knowledge so I recently purchased the complete Blaise Pascal Magazine library. I was reading an article from BPM #3 by Primož Gabrijelčič called "Introduction to Enumerators".  (See attached image)

I'm slightly confused by the following type definitions he used with regard to the TFonList class. It is declared first as an empty class and then declared a second time with private and public members.

Why is this done this way? My guess would be the private field FList in the TFontListEnumerator class needs it to be defined before it can be used.

The deeper question is how and where do you learn these class writing techniques? They seem so fundamental to writing great code.

Thanks in advance.
#classes #typedefinitions

Comments

  1. The initial declaration is a forward reference, needed because the class is a private member of the TFontListEnumerator class.

    ReplyDelete
  2. To answer your direct question, what he's doing there is called a "Forward Declaration".  The forward declaration says "Hey, I'll define TFontList later.  Until then, when you run across TFontList, hold a spot for it with pointer to a class that you can fill in later when I actually define TFontList".

    ReplyDelete
  3. Perhaps it would be helpful to point out why this is needed at all.  After all, the unspoken thought might be that "*I* can understand mutually dependent classes just fine when reading the source code, why exactly do I have to bother with this anonymous 'pre declaration' that gets filled in later in my source code for the compiler to understand it?"  

    The simple answer for this question is that Delphi is a single pass (to be clear, a single source pass) compiler.  (I don't know whether being single pass extends internally to the AST as well, but I digress.)  

    The upshot of this is that, if you didn't have the forward reference, the compiler would effectively run into the use of the "TFontList" class at a point in time where it hadn't yet seen any kind of definition for "TFontList", which would all else being equal be a problem in the code, and which would normally generate an error, possibly about an undefined identifier (or whatever the error message is.)  

    Therefore, to give the compiler a little help and avoid this situation, the compiler therefore allows you to do this so called forward reference, which is basically telling the compiler "OK I know I've not told you what this is yet, but let me just forewarn you that this is some kind of class that I'll tell you about in a minute, so please don't complain about it..."

    Multi-pass compilers (like C++) do not require this and allows you to declare classes in arbitrary order.  This sounds nice, but comes at a noticeable cost -- usually multi-pass compilers are slower, sometimes substantially slower, than single pass compilers.  

    Delphi's designers therefore took the pragmatic decision here that requiring a forward declaration in code to enable mutual class references was the better part of valor, -- an acceptable concession in order to allow the compiler to read/parse the language source in a single pass, thereby keeping the compiler as lean and fast as possible.  

    Little things like this and other language differences mean that the Delphi compiler is usually quick and sometimes vastly quicker at compiling large codebases when compared to other compilers/languages (for example C++.)

    ReplyDelete
  4. Yes. In short this is a compiler workaround to allow the code to build. Take out the forward reference and then click Compile. Now put it back. Its an important step in understanding Pascal type languages.

    As another step in deepening your knowledge, I suggest building some tool that has to parse pascal code. You can use the open source pascal parsing framework from Castalia by Jacob Thurman currently on github.

    Knowing what lexing means and what tokenization means will make you a better coder!

    ReplyDelete
  5. The reason this works is that all you're doing is declaring data types, and so all the compiler really needs to know at that stage is how to store an instance of the type.

    When it comes across "FList: TFontList", it doesn't need to know what methods TFontList may have and that stuff. It just needs to know how many bytes FList occupies so it can finish the definition of the TFontListEnumerator class. And since you told it TFontList will be a class, then it knows FList will simply be a pointer, and pointers have a nice fixed size.

    For completeness, this also works with "regular" pointers as well, for the same reason:
    type
      PRec = ^TRec; // declared before TRec
      TRec = record 
        v: integer;
        next: PRec; // TRec not fully defined yet, but pointer is
      end;

    ReplyDelete
  6. Michael Riley While I have nothing to add to the question, I would like to thank you for support the (only?) Delphi magazine out there.

    ReplyDelete
  7. I always wondered why the functions have a "forward" keyword, not classes. I usually add a comment

    type
      TFontList = class; // forward

    ReplyDelete
  8. Paul TOTH Well it's not strictly needed, unlike for functions.

    ReplyDelete
  9. Forward declaration is nice for aggregation, but unfortunately, it doesn't work with Generics.  

    To a certain degree, you can work around that by creating an abstract class without the Generic type, and let the generics class inherit from the abstract class, but it is a bit cumbersome as you have to expose all the methods and properties that you want the list to work with, without actually referencing the type.

    type
      TAbstractElement= class abstract 
      public
        constructor Create; virtual; abstract;
        function Verb:Integer; virtual; abstract;
      end;

    TGenericList = class(TList);

    TGenericElement = class(TAbstractElement)
    private
      FParent: TGenericList;
      FValue: T;
    public
        constructor Create; virtual; override;
        function Verb:Integer; virtual; override;
        property Value:T read FValue write FValue;
    end;

    The other option is using nested generics types - but that makes the creation of  descendant classes a bit more cumbersome.

    ReplyDelete
  10. Walter Prins
     >Multi-pass compilers (like C++) do not require this and allows you to declare classes in arbitrary order.  This sounds nice, but comes at a noticeable cost -- usually multi-pass compilers are slower, sometimes substantially slower, than single pass compilers.

    This is true to a certain degree, but the thing that truly makes C++ compile slowly is templates, not multi-pass compilation.  As templates are Turing-complete, the template expansion process can literally take an infinite amount of time!

    ReplyDelete
  11. Mason Wheeler Another thing is that it has to process all headers for each source file. If you use fancy libraries like STL and Boost that's quickly several thousand include files. Though precompiled headers can help a lot if you can use them.

    ReplyDelete
  12. Lars Fosdal Unlike interfaces, the use of generics seems to have caught on rather quickly. I dare to hope that we will see continuing improvements in this area, as their widespread usage makes plain the limitations in their current implementation.

    ReplyDelete

Post a Comment