Thursday, December 15, 2011

Creating an Object from an Unconstrained Generic Class

As you know if you follow my blog, OmniThreadLibrary now offers a simple way to do optimistic and pessimistic atomic initialization which works for interfaces, objects and (in the case of the pessimistic initialization), anything else. [In case you missed those articles - I also discussed a comparison of both methods and wrote a short post about the third approach to initialization.]

A typical usage of both types of initialization would be:

var
  sl: TStringList;
  ol: Locked<TObjectList>;

Atomic<TStringList>.Initialize(sl,
  function: TStringList
  begin
    Result := TStringList.Create;
  end);

ol.Initialize(
  function: TObjectList
  begin
    Result := TObjectList.Create;
  end);

As you can see, this is pretty long-winded. If you are initializing an interface, then you’ll usually have written a factory method already and the code would be much simpler (example below this paragraph) but in the case of objects this is not very typical.

Atomic<IGpIntegerList>.Initialize(list, TGpIntegerList.CreateInterface);

So I thought – both Atomic and Locked already know what entity type they wrap around so calling a constructor from inside Initialize would be trivial, eh? I could then write a simplified version

Atomic<TStringList>.Initialize(sl);
ol.Initialize;

and use the longer version only when needed, for example to initialize interfaces or to call a non-default object constructor. What could possibly go wrong?

So I tried to write simplified initializer for objects …

class function Atomic<T>.Initialize(var storage: T): T;
begin
  if PTypeInfo(TypeInfo(T))^.Kind  <> tkClass then
    raise Exception.Create('Atomic<T>.Initialize: Unsupported type');
  Result := Atomic<T>.Initialize(storage,
    function: T
    begin
      Result := T.Create;
    end);
end;

… and my plans were immediately thwarted.

[DCC Error] OtlSync.pas(671): E2003 Undeclared identifier: 'Create'

Why oh why can’t Delphi call the constructor? Well, because I cheated and declared the Atomic type without any constraints.

  Atomic<T> = class

To call the constructor I should constrain the T type to be a class with a constructor.

  Atomic<T:class,constructor> = class

So the next question is – why I cheated? It’s simple – because I wanted Atomic to wrap both objects and interfaces and in Delphi one cannot set a generic type constraint to be a union of type. This would be great but sadly it doesn’t work (it would also introduce many problems so I’m not even bothering to propose a language change in this direction):

  Atomic<T: class, constructor or IInterface> = class

Great thanks to [TOndrej] who helped me write an unconstrained generic which checks the type of T at runtime.

As usual, there’s a solution in Delphi for everything, except this time I was not smart enough to find it. Again, StackOverflow came to the rescue (and again, [TOndrej] was the hero of the day). His solution was to use

Result := T(GetTypeData(PTypeInfo(TypeInfo(T)))^.ClassType.Create);

instead of Result := T.Create;.

This worked just fine – in XE. And XE2 (original release). And XE2 Update 1. But it failed in XE2 Update 2 where casting to T was suddenly not allowed anymore. Of course, this again only fails for unconstrained types but, as I said before, I really really want T to be unconstrained in this case.

For some time it looked like I’ll have to scrap my plans for simplified initializers, but then another great mind stepped ahead and provided XE2 Update 2 compatible solution in the OmniThreadLibrary forum., Thanks, [cjsalamon]!

class function Atomic<T>.Initialize(var storage: T): T;
begin
  Result := Atomic<T>.Initialize(storage,
    function: T
    var
      inValue : TValue;
      outValue: TValue;
    begin
      inValue := GetTypeData(PTypeInfo(TypeInfo(T)))^.ClassType.Create;
      inValue.TryCast(TypeInfo(T), outValue);
      Result := outValue.AsType<T>;
    end);
end;

This solution uses TValue (which in turn uses Extended RTTI) to do the casting. Ugly, slow, but this doesn’t matter as the initialization of shared resources typically occurs very rarely. Plus you can always use the version with the explicit resource factory if you really need object creation to be swift.

Ooops!

After publishing this article, [Sergey] noted in comments that the method is flawed. [TOndrej]’s hack always calls TObject.Create and not the constructor from the T class, which makes it pretty much unusable :(

Luckily, there’s a solution for that too – use ERTTI to enumerate all T’s methods, find first parameterless constructor and execute it. It was posted in the already mentioned StackOverflow question and is now the accepted solution for the problem. Thanks, [Linas]!

So at the end my plans (again!) came to the happy conclusion thanks to great programmers all around the world. Thanks again, guys!

7 comments:

  1. >His solution was to use
    >Result := >T(GetTypeData(PTypeInfo(TypeInfo(T)))^.ClassTyp>e.Create);

    This is an inproper and unsafe solution as you use an Tobject.create (Empty initializer).

    ReplyDelete
  2. @Sergey, you are right! Do you have a solution for that?

    ReplyDelete
  3. OK, I have the solution; I'll correct the article. Thanks!

    ReplyDelete
  4. Why do you mix the semantic of types in one operation?

    - is safe solution for objects.

    Try to use overload mechanism:

    Atomic.Initialize(var storage: T): T;
    Atomic.Initialize(var storage: T): T;

    Or may be

    Atomic.Initialize(var storage: T): W;
    cast t to w.

    I have only R3 and abap near at hand. :)
    Delphi at night. :))))

    ReplyDelete
  5. Sorry,

    Use Overloads,

    Atomic.Initialize < T:class,constructor >
    (var storage: T): T;

    Atomic.Initialize < T: Iunknown > (var storage: T): T;

    Or

    Atomic.Initialize < T:class,constructor; W:Iunknown >
    (var storage: T): W;

    cast T to W via getinterface for instance.

    ReplyDelete
  6. @Sergey, AFAIK overloads don't work here.

    ReplyDelete
  7. if in a generic class with an unconstrained type parameter is initiated without specifying actual type argument ?

    ReplyDelete