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
http://docwiki.embarcadero.com/RADStudio/XE7/en/Classes_and_Objects#Forward_Declarations_and_Mutually_Dependent_Classes
ReplyDeleteThe initial declaration is a forward reference, needed because the class is a private member of the TFontListEnumerator class.
ReplyDeleteTo 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".
ReplyDeletePerhaps 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?"
ReplyDeleteThe 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++.)
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.
ReplyDeleteAs 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!
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.
ReplyDeleteWhen 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;
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.
ReplyDeleteI always wondered why the functions have a "forward" keyword, not classes. I usually add a comment
ReplyDeletetype
TFontList = class; // forward
Paul TOTH Well it's not strictly needed, unlike for functions.
ReplyDeleteForward declaration is nice for aggregation, but unfortunately, it doesn't work with Generics.
ReplyDeleteTo 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.
Walter Prins
ReplyDelete>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!
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.
ReplyDeleteLars 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