Implementing generic helpers as metaclasses

While it wasn’t the main point of the post, I did mention previously the possibility of using metaclasses as generic helpers, and how this can be preferable to using interfaces. An esteemed commentator then suggested the example I gave was far too trivial to be generalised, and in particular, that the hacky cleverness of Generics.Defaults was all very necessary. This rather sounded like a challenge, and one I should take up — so I have!

The context here is how the classes in Generics.Collections need some help to do their work, given they cannot specify operator constraints. Generics.Defaults therefore defines (and implements, for a range of standard types) two interfaces: IComparer<T> and IEqualityComparer<T>. The first of these is used by TList<T> and TObjectList<T>, the second by TDictionary<T> and TObjectDictionary<T>. Generics.Defaults also provides some abstract base classes for the interfaces. Despite the help advising you to use these classes for custom comparers, Generics.Defaults does not use them itself however: instead, it constructs the interface ‘objects’ for standard comparers manually. And, by manually, I mean manually — the VMTs are hand crafted, in the manner of doing COM development in C. Surely, one wonders, there could have been an easier way?

Well I say there was, the alternative being to use metaclasses instead of interfaces (or rather, the pretence of using interfaces!). Nonetheless, what I’m about to illustrate is for amusement purposes only: since metaclasses and interfaces are incompatible types, a metaclass approach to generic helpers isn’t one you can use with the existing TList<T> and friends — new collection classes would have to be implemented too. Moreover, the ability to construct custom comparers on the basis of an anonymous method, which the actual implementation of Generics.Defaults allows for, is forgone. On the other hand, it isn’t that difficult to derive a new class and override a single method…

That said, the first issue — and it’s quite a biggie — is that you cannot define explicit generic metaclass types. In other words, you can’t do ‘TMyObjectClass<T> = class of TMyObject<T>’ like you can do ‘TMyNonGenericClass = class of TMyNonGeneric’. In the example I gave in my previous post, this was worked around by making the helper metaclass one of the type parameters of the generic. In the case of TList<T> though, that’s surely unacceptable, so another approach is needed: an ultimate base class that is non-generic —

type
 TComparer = class;

 TComparerClass = class of TComparer;

 TComparer = class abstract
   class function CompareUntyped(const Left, Right): Integer; virtual; abstract;
   class function Default<T>: TComparerClass; static;
 end;

 TCustomComparer<T> = class abstract(TComparer)
   class function Compare(const Left, Right: T): Integer; overload; virtual;
   class function CompareUntyped(const Left, Right): Integer; overload; override;
 end;

TList<T>, then, will be using the non-generic, untyped parameter version, though since you call a method with untyped parameters in exactly the same way as a method with typed parameters, it won’t care.

That said, notice TCustomComparer<T> is marked as abstract yet has no methods left abstract. This is because the implementation of its two methods is just to call each other — descendants, then, must implement at least one of them (and typically, no more than one of them):

class function TCustomComparer<T>.CompareUntyped(const Left, Right): Integer;
begin
 Result := Compare(T(Left), T(Right));
end;

class function TCustomComparer<T>.Compare(const Left, Right: T): Integer;
begin
 Result := CompareUntyped(Left, Right);
end;

So, concrete descendants will have the choice of implementing either CompareUntyped or Compare. Given it’s CompareUntyped that will get called by TList<T>, choosing that will offer a marginal performance benefit, though it doesn’t really matter.

As for the implementation of TComparer.Default<T>, this inspects the basic RTTI of T to determine the correct concrete comparer to use (Generics.Defaults does the same thing). For the example, I haven’t defined helpers for every standard type, though those given should illustrate the point enough:

class function TComparer.Default<T>: TComparerClass;
var
  Info: PTypeInfo;
begin
  Info := TypeInfo(T);
  case Info.Kind of
    tkInteger, tkChar, tkEnumeration, tkSet, tkWChar:
      case GetTypeData(Info).OrdType of
        otUByte: Exit(TUInt8Comparer);
        otUWord: Exit(TUInt16Comparer);
        otSLong: Exit(TInt32Comparer);
        otULong: Exit(TUInt32Comparer);
      end;
   tkClass, tkInterface, tkPointer, tkClassRef, tkProcedure, tkDynArray:
     Exit({$IF SizeOf(Pointer) = 4}TUInt32Comparer{$ELSE}TUInt64Comparer{$IFEND});
   tkInt64: Exit(TInt64Comparer);
   tkUString: Exit(TUnicodeStringComparer);
 end;
 Result := TBinaryComparer<T>;
end;

The reuse of integer helpers for other ordinal types (something actually afforded by CompareUntyped) is something Generics.Defaults does too. The helpers themselves are declared as thus:

type
  TBinaryComparer = class(TCustomComparer<T>)
    class function CompareUntyped(const Left, Right): Integer; override;
  end;

  TUInt8Comparer = class(TCustomComparer<UInt8>) //Byte, AnsiChar
    class function CompareUntyped(const Left, Right): Integer; override;
  end;

  TUInt16Comparer = class(TCustomComparer<UInt16>) //Word, WideChar
    class function CompareUntyped(const Left, Right): Integer; override;
  end;

  TInt32Comparer = class(TCustomComparer<Int32>)
    class function CompareUntyped(const Left, Right): Integer; override;
  end;

  TUInt32Comparer = class(TCustomComparer<UInt32>)
    class function CompareUntyped(const Left, Right): Integer; override;
  end;

  TInt64Comparer = class(TCustomComparer<Int64>)
    class function CompareUntyped(const Left, Right): Integer; override;
  end;

  TUInt64Comparer = class(TCustomComparer<UInt64>)
    class function CompareUntyped(const Left, Right): Integer; override;
  end;

  TUnicodeStringComparer = class(TCustomComparer<UnicodeString>)
    class function Compare(const Left, Right: UnicodeString): Integer; override;
  end;

  TStringComparer = TUnicodeStringComparer;

The implementations are as you would expect (note that BinaryCompare is provided by Generics.Defaults):

class function TBinaryComparer<T>.CompareUntyped(const Left, Right): Integer;
begin
  Result := BinaryCompare(@Left, @Right, SizeOf(T));
end;

class function TUInt8Comparer.CompareUntyped(const Left, Right): Integer;
begin
  if UInt8(Left) < UInt8(Right) then
    Result := -1
  else if UInt8(Left) > UInt8(Right) then
    Result := 1
  else
    Result := 0;
end;

class function TUInt16Comparer.CompareUntyped(const Left, Right): Integer;
begin
  if UInt16(Left) < UInt16(Right) then
    Result := -1
  else if UInt16(Left) > UInt16(Right) then
    Result := 1
  else
    Result := 0;
end;

class function TInt32Comparer.CompareUntyped(const Left, Right): Integer;
begin
  if Int32(Left) < Int32(Right) then
    Result := -1
  else if Int32(Left) > Int32(Right) then
    Result := 1
  else
    Result := 0;
end;

class function TUInt32Comparer.CompareUntyped(const Left, Right): Integer;
begin
  if UInt32(Left) < UInt32(Right) then
    Result := -1
  else if UInt32(Left) > UInt32(Right) then
    Result := 1
  else
    Result := 0;
end;

class function TInt64Comparer.CompareUntyped(const Left, Right): Integer;
begin
  if Int64(Left) < Int64(Right) then
    Result := -1
  else if Int64(Left) > Int64(Right) then
    Result := 1
  else
    Result := 0;
end;

class function TUInt64Comparer.CompareUntyped(const Left, Right): Integer;
begin
  if UInt64(Left) < UInt64(Right) then
    Result := -1
  else if UInt64(Left) > UInt64(Right) then
    Result := 1
  else
    Result := 0;
end;

class function TUnicodeStringComparer.Compare(const Left, Right: UnicodeString): Integer;
begin
  Result := AnsiCompareStr(Left, Right);
end;

That’s about it really, other than for a new TList<T> implementation to test against:

type
  TList<T> = class
  private
    FItems: array of T;
    FCount: Integer;
    FComparer: TComparerClass;
    function GetItem(Index: Integer): T;
  public
    constructor Create(const AComparer: TComparerClass = nil); overload;
    function Add(const Value: T): Integer;
    function IndexOf(const Value: T): Integer;
    property Count: Integer read FCount;
    property Items[Index: Integer]: T read GetItem; default;
  end;

constructor TList<T>.Create(const AComparer: TComparerClass);
begin
  inherited Create;
  if AComparer <> nil then
    FComparer := AComparer
  else
    FComparer := TComparer.Default<T>;
end;

function TList<T>.Add(const Value: T): Integer;
begin
  if Length(FItems) = FCount then
    SetLength(FItems, FCount + 8);
  FItems[FCount] := Value;
  Result := FCount;
  Inc(FCount);
end;

function TList<T>.GetItem(Index: Integer): T;
begin
  if (Index < 0) or (Index >= FCount) then
    raise EArgumentOutOfRangeException.CreateRes(@SArgumentOutOfRange);
  Result := FItems[Index];
end;

function TList<T>.IndexOf(const Value: T): Integer;
begin
  for Result := 0 to Count - 1 do
    if FComparer.CompareUntyped(FItems[Result], Value) = 0 then Exit;
  Result := -1;
end;

Obviously, that’s not a complete implementation (and yes, Add does use a stupid allocation strategy), but it’s enough to test against:

var
  IntList: TList<Integer>;
  EnumList: TList<TNameType>;
  StringList: TList<string>;
  RectList: TList<TRect>;
begin
  ReportMemoryLeaksOnShutdown := True;
  //this should end up outputting '1' four times.
  try
    IntList := TList<Integer>.Create;
    try
      IntList.Add(42);
      IntList.Add(999);
      IntList.Add(123);
      Writeln(IntList.IndexOf(999));
    finally
      IntList.Free;
    end;
    EnumList := TList<TNameType>.Create;
    try
      EnumList.Add(ntDcpBpiName);
      EnumList.Add(ntRequiresPackage);
      EnumList.Add(ntContainsUnit);
      Writeln(EnumList.IndexOf(ntRequiresPackage));
    finally
      EnumList.Free;
    end;
    StringList := TList<string>.Create;
    try
      StringList.Add('hello');
      StringList.Add('metaclass');
      StringList.Add('world');
      Writeln(StringList.IndexOf('metaclass'));
    finally
      StringList.Free;
    end;
    RectList := TList<TRect>.Create;
    try
      RectList.Add(Rect(1, 2, 3, 44));
      RectList.Add(Rect(1, 2, 3, 4));
      RectList.Add(Rect(100, 200, 656, 2340));
      Writeln(RectList.IndexOf(Rect(1, 2, 3, 4)));
    finally
      RectList.Free;
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  ReadLn;
end.

So, all told, is the metaclass approach completely and utterly ‘clean’? Ultimately not, given the need to work around the compiler not supporting generic metaclass types. Moreover, and due to another limitation in the compiler, all the concrete types need to be declared in the interface section of the unit. On the other hand, if the untyped parameters offend you, the various TXXXComparer classes could be switched to override the typed Compare method instead with no functional change. And, as for the need to declare in the interface section, this does mean a customised string comparer (for example) can actually descend from the standard one, which isn’t the case with the real Generics.Defaults implementation.

All told, I think my original claim stands up — metaclasses do serve as a cleaner and less heavyweight alternative to interfaces when it comes to defining generic helpers. Feel free to disagree in the comments though, especially as the code I’ve presented is something I’ve just whipped up.

Advertisements

6 thoughts on “Implementing generic helpers as metaclasses

  1. The problem with this approach is that in some cases you would like to have some “state” in the comparer which requires an instance (ex. TStringComparer that can be case sensitive or insensitive). The example is not too good since you can create two different versions of the comparer … but in case of a state modifier with many possible values it becomes a mess.

    But for 90% of cases this approach beats interfaces.

  2. I’ve been using a similar approach for a while, apart from bypassing the reference-counting issues of interface, the one huge overwhelming (IME) advantage is that meta-classes are well defined, and that can simplify debugging and maintenance tremendously.

    Interfaces on the other sides can be implemented in a myriad of ways (including manually constructed out of the blue, or can refer incorrectly freed objects, etc.), so when something goes wrong and all the clue you have is an interface, you can sometimes face a debugging nightmare.

  3. Boa noite . parábens pelo tema abordado . Porém estou com dificuldade de simular o exemplo . Acho que o editor do wordpress corrompeu a sintaxi correta . O senhor não poderia fazer a gentileza para um amigo brasileiro e zipar este exemplo , para que eu possa aprofundar meus estudos neste assunto . mais uma vez muito obrigado

    marcosalles.wordpress.com

  4. Good evening. Congratulations theme. But I’m having difficulty simulating the example. I think the editor of wordpress corrupted sintaxi correct. You could not do a kindness to a Brazilian friend and zipping this example, so I can further my studies in this subject. Once again thank you very much

    marcosalles@outlook.com.br

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s