Primož GabrijelÄiÄ 227 Posted January 29, 2019 (edited) Original post:Ā https://www.thedelphigeek.com/2019/01/caching-with-class-variables.html Recently I was extending a swiss-army-knife helper record we are using at work and I noticed a small piece of suboptimal code. At first I let it be as I couldnāt think of a simple way of improving the code ā and the problem was really not so big that it should be fixed with a complicated solution. At some point, however, a simple and elegant solution appeared to me and I like it so much that I want to share it with the world š Ā 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; Ā CallingĀ Range<TEnum>(i)Ā works the same as executingĀ TEnum(i)Ā with an added bonus of checking for under- and overflows. The following code fragment shows how this function could be used: 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 Ā TheĀ CheckĀ function uses following two helper functions to determine lowest and highest possible value for typeĀ T: 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; Ā The suboptimal behaviour comes from the fact thatĀ MinIntValĀ andĀ MaxIntValĀ are calculated each timeĀ CheckĀ is called. As typeĀ TĀ doesnāt change while the program is being executed, it would suffice to call these two functions once and cache the result. The problem with this solution, however, is twofold. Firstly, this cache would have to exist somewhere. Some part of code would have to manage it. Secondly, it would have to be quite fast.Ā MinIntValĀ andĀ MaxIntVal, as implemented now, are not very slow and looking up that data in a cache could easily be slower than the current code. 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; Ā AĀ class constructorĀ is called only once for each typeĀ TĀ used in the code. It is also called automatically and we donāt have to take care of that. Moving the code that calculates min/max values for a typeĀ TĀ into a class constructor therefore solves the first problem. To make sure thatĀ classĀ part of theĀ TypeInfoCache<T>Ā was created, we merely have to access it, nothing more. The code inĀ Range<T>Ā can be replaced with simple one-liners: class function Range<T>.MaxIntVal: Integer begin Result := TypeInfoCache<T>.MaxIntVal; end; class function Range<T>.MinIntVal: Integer; begin Result := TypeInfoCache<T>.MinIntVal; end; Ā This also solves the second problem, as the access to a class variables doesnāt require any complications usually associated with a dictionary access. AccessingĀ MinIntVal,Ā for example, is a simpleĀ callĀ into method that executes fewĀ movĀ instructions. Ā 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. Edited January 30, 2019 by Primož GabrijelÄiÄ Share this post Link to post
TiGü 21 Posted January 30, 2019 Hi Primož,  that's a nice piece of code, but i notice in my Delphi Tokyo, that the ens-Result for the TEnumSet (for valid values) is empty. But if you add a untyped Move, than it works:  program Project1; {$APPTYPE CONSOLE} {$R *.res} uses System.TypInfo, System.Rtti, System.SysUtils; resourcestring SValueLiesOutsideAllowedRange = 'Value %d lies outside allowed range for %s (%d .. %d)'; type TypeInfoCache<T> = class class var FMinIntVal: Integer; FMaxIntVal: Integer; FIsSet: Boolean; public class constructor Create; class property MaxIntVal: Integer read FMaxIntVal; class property MinIntVal: Integer read FMinIntVal; class property IsSet: Boolean read FIsSet; end; Range<T> = record private class function MaxIntVal: Integer; static; inline; class function MinIntVal: Integer; static; inline; class procedure RaiseException(const Value: Integer); static; public class function Check(const Value: Integer): T; static; end; { Range<T> } class function Range<T>.Check(const Value: Integer): T; begin if (Value < MinIntVal) or (Value > MaxIntVal) then RaiseException(Value); if TypeInfoCache<T>.IsSet then begin Move(Value, Result, SizeOf(T)); // here is the magic end; 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; class procedure Range<T>.RaiseException(const Value: Integer); begin raise Exception.CreateFmt(SValueLiesOutsideAllowedRange, [Value, PTypeInfo(TypeInfo(T)).Name, MinIntVal, MaxIntVal]); end; { TypeInfoCache<T> } class constructor TypeInfoCache<T>.Create; var ti: PTypeInfo; typeData: PTypeData; i: Integer; begin ti := TypeInfo(T); FIsSet := ti.Kind = tkSet; if FIsSet then ti := GetTypeData(ti).CompType^; typeData := GetTypeData(ti); FMinIntVal := typeData.MinValue; if FIsSet then begin FMaxIntVal := 0; for i := typeData.MinValue to typeData.MaxValue do FMaxIntVal := FMaxIntVal or (1 shl i); end else FMaxIntVal := typeData.MaxValue; end; type TEnum = (en1, en2, en3); TEnumSet = set of TEnum; var en: TEnum; ens: TEnumSet; begin try try en := Range<TEnum>.Check(0); en := Range<TEnum>.Check(2); en := Range<TEnum>.Check(3); except on E: Exception do Writeln('Expected exception: ', E.ClassName, ' ', E.Message); end; try ens := Range<TEnumSet>.Check(0); ens := Range<TEnumSet>.Check(2); ens := Range<TEnumSet>.Check(7); ens := Range<TEnumSet>.Check(8); except on E: Exception do Writeln('Expected exception: ', E.ClassName, ' ', E.Message); end; except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; if DebugHook <> 0 then Readln; end.  1 Share this post Link to post
Primož GabrijelÄiÄ 227 Posted January 30, 2019 Exactly - I somehow deleted that line from the gist, while still keeping it in my local copy of the code. š š š Ā (Actually, this Move should not be 'if'-ed. Just always execute it.) Ā I'll fix my blog, gist example and this post when I come home later today. Ā Thank you for finding the bug! 1 Share this post Link to post
Attila Kovacs 660 Posted January 30, 2019 I've lost the thread after this line "As it turns out, we can fix both problems simply by usingĀ class variables, properties, and methodsĀ functionality of the Delphi language:" The same code follows as already above the paragraph was., instead of the declaration of "TypeInfoCache" I think. 1 Share this post Link to post
Stefan Glienke 2141 Posted January 30, 2019 (edited) The approach to store Min and Max Value for sets does not work as sets in Delphi can be max size of 32 Byte (set of Byte) - good luck storing that in an Integer. Ā The core idea though is nice - I think I have also done this before: use a generic type as storage for type related values. Edited January 30, 2019 by Stefan Glienke Share this post Link to post
Primož GabrijelÄiÄ 227 Posted January 30, 2019 It works for our code šĀ We don't pass large sets through that function and everything is fine.Ā Share this post Link to post
Primož GabrijelÄiÄ 227 Posted January 30, 2019 2 hours ago, Attila Kovacs said: I've lost the thread after this line "As it turns out, we can fix both problems simply by usingĀ class variables, properties, and methodsĀ functionality of the Delphi language:" The same code follows as already above the paragraph was., instead of the declaration of "TypeInfoCache" I think. Copy&paste bug, sorry. Was OK on the blog, wrong here in the forum. Corrected. Ā Thank you! Share this post Link to post
Stefan Glienke 2141 Posted January 30, 2019 39 minutes ago, Primož GabrijelÄiÄ said: We don't pass large sets through that function and everything is fine. Been there, done that. And one day the enum gets its 33. element and stuff subtly starts failing šĀ Share this post Link to post
Primož GabrijelÄiÄ 227 Posted January 30, 2019 Don't think so as we don't use crazily big sets. Plus this code is mostly used for compatibility reason; because some old configuration files store enum and set data as integers. Ā But of course, all code that can fail, will fail. Share this post Link to post
Attila Kovacs 660 Posted January 30, 2019 Ā 19 minutes ago, Stefan Glienke said: And one day the enum gets its 33 Actually atĀ 32 will already fail, because he uses integer instead of cardinal. But indeed, anĀ "if typeData.MaxValue > 30 then raise.." would make this approach safer. Share this post Link to post
Kryvich 174 Posted January 30, 2019 @Attila KovacsĀ if typeData.MaxValue > 30 thenĀ "change me to UInt64". Share this post Link to post
Attila Kovacs 660 Posted January 30, 2019 @KryvichĀ Would be an improvement yes, but still too few bits. Share this post Link to post
Primož GabrijelÄiÄ 227 Posted February 22, 2019 Class constructors are called when units are initialized, strictly in a single-threaded context. Ā Share this post Link to post