This is what I love about Delphi. After almost 15 years of Delphi’ing there’s still new things to learn, and I don’t just mean new features in the latest releases. I mean, basic, fundamental things that have been there for years, just undiscovered (by me). In this case the discovery was a little annoying, but never-the-less educational.
In the case in question I was creating some library code for myself to help with some resource manipulation code that I have in mind. As part of this I wanted an enum type to represent the normal resource types found in Windows code:
type TResourceType = ( rtCURSOR = 1, rtBITMAP, rtICON, rtMENU, rtDIALOG, rtSTRING, rtFONTDIR, rtFONT, rtACCELERATOR, rtRCDATA, rtMESSAGETABLE, rtGROUP_CURSOR, rtGROUP_ICON = 14, rtVERSION = 16, rtDLGINCLUDE, rtPLUGPLAY = 19, rtVXD, rtANICURSOR, rtANIICON, rtHTML, rtMANIFEST ); TResourceTypes = set of TResourceType;
I used specific ordinal assignments to some of the enum members to create the necessary “gaps” for the resource type values that aren’t (currently) meaningful in Windows and ensure that the ordinal values of the enum members corresponded exactly to Windows resource type values.
The enum member names are also carefully chosen so that they may be quickly converted to the symbolic names of the corresponding Windows resource type identifiers, e.g.:
rtCURSOR >> RT_CURSOR
rtGROUP_ICON >> RT_GROUP_ICON
The plan was to have a function to take a TResourceType value, convert to the enum name and change the “rt” prefix to “RT_”. This is not exactly rocket science and I did not anticipate any problems.
So I was somewhat surprised to find myself thwarted by a compilation error in the code:
sName := GetEnumName(TypeInfo(TResourceType), Ord(aValue));
ERROR: E2134 – Type ‘TResourceType’ has no typeinfo’
It took me a while to chase this down – my initial thought was to suspect that there was some other definition of a TResourceType type that was hiding my type. That was soon ruled out and a big of further digging eventually revealed the answer.
It turns out that assigning specific ordinal values to enum members (if they are different to the ordinals that the compiler would normally assign automatically) defeats the RTTI system and prevents the creation of type info for the type. Sadly this doesn’t seem to be mentioned in the documentation, not even for the E2134 error itself which would seem to be an ideal help topic to contain this useful piece of information.
The solution in my case was quite simple. The number of “unused” members was small so I could change my enum type to:
type TResourceType = ( rtNotUsed1, // = 0 rtCURSOR, rtBITMAP, rtICON, rtMENU, rtDIALOG, rtSTRING, rtFONTDIR, rtFONT, rtACCELERATOR, rtRCDATA, rtMESSAGETABLE, rtGROUP_CURSOR, rtNotUsed2, // = 13 rtGROUP_ICON, rtNotUsed3, // = 15 rtVERSION, rtDLGINCLUDE, rtNotUsed4, // = 18 rtPLUGPLAY, rtVXD, rtANICURSOR, rtANIICON, rtHTML, rtMANIFEST ); TResourceTypes = set of TResourceType;
Notice the additional rtNotUsedN members which fill in the gaps.
This created a slight problem for me in that now I had some undesirable names “polluting” my enum space. In this case I was not too concerned about that – they are self documenting after all (“NotUsed” is pretty unambiguous on that score).
But there were occasions in my code where I was going to wish to test some TResourceType value to ensure that it was valid.
A simple pair of sets helped out with this:
const rtInvalid = [rtNotUsed1, rtNotUsed2, rtNotUsed3, rtNotUsed4]; rtValidResourceTypes = [Low(TResourceType)..High(TResourceType)] - rtInvalid;
Which allowed me to perform runtime checking of some arbitrary TResourceType value:
if NOT (aValue in rtValidResourceTypes) then // Invalid resource type ... !
Or of course alternatively:
if (aValue in rtInvalid) then // Invalid resource type ... !
According to taste (you can probably tell which I prefer as the choice of names for the sets yields one which reads quite fluently and naturally, whilst the other is awkward and stilted in comparison, albeit more brief). 🙂
We Don’t Know What We Don’t Know
It’s really quite astonishing to me that in almost 15 years of working with Delphi I’ve not previously encountered this limitation of the type information system (as it relates to enum types). So astonishing that I thought it worth not just blogging about but also posting to stackoverflow as a question with a ready made answer.
I’ve only recently started using stackoverflow seriously – I hope this sort of self-answering isn’t an abuse of the system. I thought it a useful way of adding to the “knowledgebase”.
In any event, Barry Kelly, a CodeGear compiler engineer who works on the Delphi compiler itself no less (some guys get all the luck!) responded to my stackoverflow question+answer by explaining some of the internal details of the RTTI system that makes typeinfo difficult/impossible for such non-contiguous enum types, and offered the opinion that it probably wasn’t worth going to too much bother for since the times when it would be useful were so rare.
I’d have to agree with him.
It’s taken me 15 years to stumble across it – I think I can manage without a “fix” in this case.
If this had been a somewhat simpler case where I simply wanted the initial member of the enum to have a value such as 1 (one) rather than the usual 0 (zero), I could have achieved type safety with a simple subrange type (thanks to Ulrich for pointing this out on stackoverflow) renaming the underlying type and using the subrange type as the “concrete” representation of my enum throughout my code (having established the principle, in this final example I have artifically reduced the number of members in the enum type, simply to keep it short and concise):
type TResourceTypeValue = ( rtUnused, rtCURSOR, rtBITMAP, rtICON, rtMENU ); TResourceType = rtCURSOR..High(TResourceTypeValue); TResourceTypes = set of TResourceType;