Primož Gabrijelčič 223 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č 223 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 631 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 2019 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č 223 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č 223 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 2019 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č 223 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 631 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 165 Posted January 30, 2019 @Attila Kovacs if typeData.MaxValue > 30 then "change me to UInt64". Share this post Link to post
Attila Kovacs 631 Posted January 30, 2019 @Kryvich Would be an improvement yes, but still too few bits. Share this post Link to post
Primož Gabrijelčič 223 Posted February 22, 2019 Class constructors are called when units are initialized, strictly in a single-threaded context. Share this post Link to post