I casually suggested the use of the “absolute” keyword in response to a question on the NZ DUG mailing list today. I thought nothing of it but someone mentioned that it had been years since he’d seen anyone use it, so I thought maybe it was worth bringing to wider attention.
absolute is one of those language features that I’m sure will divide the community into those who think it’s a useful tool and those who think it should be avoided like a modern day goto. I’m firmly in the former camp.
absolute allows you to direct the compiler to treat one or more variables as existing at the same memory location, effectively overlaying one (or more) on top of another.
The potential for making mistakes as a result of using such a technique is, I think, fairly clear, but in my view is not that different from the dangers of unchecked typecasting (or rather pre-checked hard casting). That is where a type check is performed and then once the type identity determined, then simply assumed for the following code.
It might help to explain what I mean by that with an example.
Imagine an event handler that responds to changes in a number of controls on the form where the precise identity of each control involved isn’t important, but the type of the control determines how the event responds. There being sufficient commonality in the event handler that maintaining separate events is undesirable. Let us say for example that all editable controls on the form should enforce uppercase text entry (please bear in mind that this is an example contrived to show the potential use of absolute – don’t nit-pick the example itself):
procedure TMyForm.EditableChange(Sender: TObject); begin if Sender is TEdit then begin TEdit(Sender).Text := Uppercase(TEdit(Sender).Text); end else if Sender is TMemo then begin TMemo(Sender).Text := Uppercase(TMemo(Sender).Text); end else if (Sender is TComboBox) and (TComboBox(Sender).Style = csDropDown) then begin TComboBox(Sender).Text := Uppercase(TComboBox(Sender).Text); end; SetDirtyFlag(TRUE); end;
Now, one way of “cleaning this up” might be:
procedure TMyForm.EditableChange(Sender: TObject); var edit: TEdit; memo: TMemo; combo: TComboBox; begin if Sender is TEdit then begin edit := TEdit(Sender); edit.Text := Uppercase(edit.Text); end else if Sender is TMemo then begin memo := TMemo(Sender); memo.Text := Uppercase(memo.Text); end else if (Sender is TComboBox) then begin combo := TComboBox(Sender); if (combo.Style = csDropDown) then begin combo.Text := Uppercase(combo.Text); end; end; SetDirtyFlag(TRUE); end;
I don’t know about you, but I think the cure in this case is worse than the disease. So now let’s see how ‘absolute‘ makes this attempt to clean up far more successful:
procedure TMyForm.EditableChange(Sender: TObject); var control: TObject; edit: TEdit absolute control; memo: TMemo absolute control; combo: TComboBox absolute control; begin control := Sender; if Sender is TEdit then begin edit.Text := Uppercase(edit.Text); end else if Sender is TMemo then begin memo.Text := Uppercase(memo.Text); end else if (Sender is TComboBox) and (combo.Style = csDropDown) then begin combo.Text := Uppercase(combo.Text); end; SetDirtyFlag(TRUE); end;
This is just as type-safe as each previous effort – type checking is being performed as required. The difference is that the type-casting is now effectively being performed in the declaration of the various variables themselves.
The absolute keyword tells the compiler to treat edit, memo and combo as simply different types of variable all located in the same location in memory as the control variable.
But more than that – to my mind it also acts as a clear indicator in the declaration of those variables themselves as to how those variables are used in the function or procedure body that follows, in a way that is not at all clear (without carefully reading the details of the code itself) in the previous example.
I like code that explains itself – it means I don’t have to write as much in the comments!
Now, in fact things can be made just as clear and a little less verbose, since as well as “overlaying” variables on top of one another, the absolute keyword can be used to “overlay” variables on parameters themselves, so in this case we can eliminate that rather distracting control variable:
procedure TMyForm.EditableChange(Sender: TObject); var edit: TEdit absolute Sender; memo: TMemo absolute Sender; combo: TComboBox absolute Sender; begin if Sender is TEdit then begin edit.Text := Uppercase(edit.Text); end else if Sender is TMemo then begin memo.Text := Uppercase(memo.Text); end else if (Sender is TComboBox) and (combo.Style = csDropDown) then begin combo.Text := Uppercase(combo.Text); end; SetDirtyFlag(TRUE); end;
As I said, this is a not very nice and very contrived example, but I wanted to use something simple to introduce the idea before complicating things just a little.
Mutating the Identity of Schizophrenic Types
If you’ve ever had to work with resources in your applications – icons, bitmaps etc – you will no doubt be familiar with MAKEINTRESOURCE(). This little function converts a 16-bit resource ID into a value that can be used in place of a pointer to a resource name in some functions.
It also means that sometimes you will received a pointer to a resource name that is in fact not a pointer at all but one of these specially formatted resource ID values and needing to deal with it according to what the value contains – it could be a 16-bit integer or a pointer to a string.
This is potentially fiddly, but absolute makes it simple and, again, clearly states the intent of your code without requiring you to explicitly implement code to execute that intent:
function GetResourceIDAsString(const aID: PChar): String; var id: Integer absolute aID; begin if HiWord(aID) = 0 then result := IntToStr(id) else result := aID; end;
Note: the test of the high 16-bits of the parameter passed could equally have been written as simply:
if id <= MAXWORD then
Notice how this technique preserves the type safety of the supplied parameter (resource ID’s are treated as PChar) without involving any messy typecasts in the implementation itself. By contrast I think a common approach that you might encounter that didn’t use absolute would look something like:
function GetResourceIDAsString(const aID: PChar): String; var dw: DWORD; begin dw := DWORD(aID); if HiWord(dw) = 0 then result := IntToStr(LoWord(dw)) else result := aID; end;
Not a huge difference maybe (and a trivial, albeit representatively ‘real’, example in this case), but we have declared a variable and required a typecast for no good reason (in terms of the purpose of the function) than to allow us to work with the supplied value in a particular way. I suspect that in such a case the temptation would be to actually do away with the variable and repeatedly typecast:
function GetResourceIDAsString(const aID: PChar): String; begin if HiWord(DWORD(aID)) = 0 then result := IntToStr(LoWord(DWORD(aID))) else result := aID; end;
Which is shorter, no doubt but… well, perhaps some people like this sort of repetitive and heavily parenthesised code. I personally don’t.
To End At The Beginning
As I said at the outset, this all started when I responded to a question on the NZ DUG mailing list. The question in, uh, question was about how to take advantage of the for in language construct introduced in Delphi 2007. The case involved trying to use for in to iterate over the contents of an instance of existing class derived from TObjectList – i.e. no custom enumerator had yet been provided (and bear in mind that generics weren’t available as the Delphi version in question does not support them in Win32 code).
The question was asking why compilation errors resulted from code such as this (slightly simplified from the original question, the loop body in particular) and how to avoid them:
procedure PrintFormats( aFormatList: TAddressFormatList ); var addrFormat: TAddressFormat; begin for addrFormat in addrFormats do WriteLn( addrFormat.Description ); end;
Since TAddressFormatList derived from TObjectList, this was failing with an incompatible types error, since the enumerator for TObjectList yields Pointer values, not TAddressFormat references.
The question was posed by someone who had seen someone demo the language feature by writing a for in loop to process the contents of a TStringList but who hadn’t perhaps fully appreciated the mechanism by which this language feature works and the infrastructure they needed to provide in order to make it work (which is of course provided by the VCL in the case of certain types already).
You could of course simply do as the TObjectList required, and enumerate using a pointer variable, type-casting each yielded value in the body of the loop:
procedure PrintFormats( aFormatList: TAddressFormatList ); var addrFormat: Pointer; begin for addrFormat in aFormatList do WriteLn( TAddressFormat(addrFormat).Description ); end;
But with numerous uses of addrFormat in the loop this becomes messy. So you might introduce another, strongly typed variable, do the typecast once into that variable and then use that other variable in the body of the loop:
procedure PrintFormats( aFormatList: TAddressFormatList ); var addrFormatEnum: Pointer; addrFormat: TAddressFormat; begin for addrFormatEnum in aFormatList do begin addrFormat := TAddressFormat(addrFormatEnum); WriteLn(addrFormat.Description ); end; end;
But we’ve already seen how absolute helps tidy this up even further and make the relationship between those variables absolutely [sic] crystal clear into the bargain:
procedure PrintFormats( aFormatList: TAddressFormatList ); var addrFormatEnum: Pointer; addrFormat: TAddressFormat absolute addrFormatEnum; begin for addrFormatEnum in aFormatList do begin WriteLn(addrFormat.Description ); end; end;
As I say, expect there will be a number of comments to this pointing out the dangers of this technique, but I believe those dangers to be no greater, and in some ways somewhat less, than those of unchecked typecasts, which is what absolute essentially replaces. In it’s favour it helps eliminate “noise” and in my view makes the intent of variables clearer without requiring the reading of code to determine that usage.
If nothing else, it may be another tool in your toolbox. Or perhaps a tool that you didn’t even realise you had at your disposal.
I also find it somewhat gratifying that this language feature, I think among one of the oldest to still survive in the language, can help make one of the newest language features more usable without having to spend time writing code that isn’t directly related to the business problem at hand (writing enumerators and working with enumerated lists without the benefit of generics or perhaps where a suitably typed enumerator simply hasn’t been provided).