Instead of showing our helper record in full, I have extracted just a small part of functionality, enough to do a simple demo. The Check method takes an integer, makes sure that it can be cast into an enumerated type or set T and returns the cast type:
type Range<T> = record private class function MaxIntVal: Integer; static; inline; class function MinIntVal: Integer; static; inline; public class function Check(const value: Integer): T; static; end; class function Range<T>.Check(const value: Integer): T; begin if (value < MinIntVal) or (value > MaxIntVal) then raise Exception.CreateFmt( 'Value %d lies outside allowed range for %s (%d .. %d)', [value, PTypeInfo(TypeInfo(T)).Name, MinIntVal, MaxIntVal]);
Move(value, Result, SizeOf(Result));
end;
type TEnum = (en1, en2, en3); TEnumSet = set of TEnum; var en: TEnum; ens: TEnumSet; en := Range<TEnum>.Check(2); // OK, en = en3 en := Range<TEnum>.Check(3); // exception ens := Range<TEnumSet>.Check(0); // OK, ens = [] ens := Range<TEnumSet>.Check(8); // exception
class function Range<T>.MaxIntVal: Integer; var ti: PTypeInfo; typeData: PTypeData; isSet: Boolean; i: Integer; begin ti := TypeInfo(T); isSet := ti.Kind = tkSet; if isSet then ti := GetTypeData(ti).CompType^; typeData := GetTypeData(ti); if isSet then begin Result := 0; for i := typeData.MinValue to typeData.MaxValue do Result := Result or (1 shl i); end else Result := typeData.MaxValue; end; class function Range<T>.MinIntVal: Integer; var ti: PTypeInfo; typeData: PTypeData; begin ti := TypeInfo(T); if ti.Kind = tkSet then ti := GetTypeData(ti).CompType^; typeData := GetTypeData(ti); Result:= typeData.MinValue; end;
As it turns out, we can fix both problems simply by using class variables, properties, and methods functionality of the Delphi language:
type TypeInfoCache<T> = class class var FMinIntVal: Integer; FMaxIntVal: Integer; public class constructor Create; class property MaxIntVal: Integer read FMaxIntVal; class property MinIntVal: Integer read FMinIntVal; end; class constructor TypeInfoCache<T>.Create; var ti: PTypeInfo; typeData: PTypeData; isSet: Boolean; i: Integer; begin ti := TypeInfo(T); isSet := ti.Kind = tkSet; if isSet then ti := GetTypeData(ti).CompType^; typeData := GetTypeData(ti); FMinIntVal := typeData.MinValue; if isSet then begin FMaxIntVal := 0; for i := typeData.MinValue to typeData.MaxValue do FMaxIntVal := FMaxIntVal or (1 shl i); end else FMaxIntVal := typeData.MaxValue; end;
class function Range<T>.MaxIntVal: Integer; begin Result := TypeInfoCache<T>.MaxIntVal; end; class function Range<T>.MinIntVal: Integer; begin Result := TypeInfoCache<T>.MinIntVal; end;
A demonstration project for this new improved solution is available here.
This approach is very limited in use – it can only be used to associate data with a type T – but neatly illustrates the power of the Delphi language.
Indeed, nice.
ReplyDeleteKind of a trend in modern languages to have a pair of class + singular object with the same name.
That perhaps can be used instead of "non-inheritable class variables" that are typically used by hijacking VMT
> raise Exception.CreateFmt
ReplyDeletewhy not standard ERangeError ?
Whatever. Definitely not the point of this exercise.
DeleteNice idea to have some kind of type traits class that gets initialized by a class constructor.
ReplyDeleteThanks for sharing! I can get the point (the caching part), however, I'm having difficulty understanding the part that calculates the max value for a set - especially the use of `shl`, anyone care to explain a little bit :)
ReplyDelete`
if isSet then
begin
FMaxIntVal := 0;
for i := typeData.MinValue to typeData.MaxValue do
FMaxIntVal := FMaxIntVal or (1 shl i);
end
`