4

I'm working on a project containing several packages. In one of my base packages I declare a smart pointer, like that (here is the complete code):

unit UTWSmartPointer;

interface

type
    IWSmartPointer<T> = reference to function: T;

    TWSmartPointer<T: class, constructor> = class(TInterfacedObject, IWSmartPointer<T>)
    private
        m_pInstance: T;

    public
        constructor Create; overload; virtual;

        constructor Create(pInstance: T); overload; virtual;

        destructor Destroy; override;

        function Invoke: T; virtual;
    end;

implementation
//---------------------------------------------------------------------------
constructor TWSmartPointer<T>.Create;
begin
    inherited Create;

    m_pInstance := T.Create;
end;
//---------------------------------------------------------------------------
constructor TWSmartPointer<T>.Create(pInstance: T);
begin
    inherited Create;

    m_pInstance := pInstance;
end;
//---------------------------------------------------------------------------
destructor TWSmartPointer<T>.Destroy;
begin
    m_pInstance.Free;
    m_pInstance := nil;

    inherited Destroy;
end;
//---------------------------------------------------------------------------
function TWSmartPointer<T>.Invoke: T;
begin
    Result := m_pInstance;
end;
//---------------------------------------------------------------------------

end.

Later in my project (and in another package), I use this smart pointer with a GDI+ object (a TGpGraphicsPath). I declare the graphic path like that:

...
pGraphicsPath: IWSmartPointer<TGpGraphicsPath>;
...
pGraphicsPath := TWSmartPointer<TGpGraphicsPath>.Create();
...

However, nothing is drawn on the screen when I execute the code. I get no error, no exception or access violation, just a blank page. But if I just change my code like that:

...
pGraphicsPath: IWSmartPointer<TGpGraphicsPath>;
...
pGraphicsPath := TWSmartPointer<TGpGraphicsPath>.Create(TGpGraphicsPath.Create);
...

then all become fine, and my path is painted exactly as expected. But I cannot figure out why the first constructor does not work as expected. Somebody can explain to me this strange behavior?

Regards

Jean-Milost Reymond
  • 1,833
  • 1
  • 15
  • 36
  • 3
    My guess would be that the TGpGraphicsPath takes an optional argument, in which case TObject.Create will be called instead of TGpGraphicsPath.Create( optionalArgument ) - something that you have to watch out for with generics. – Dsm Feb 15 '17 at 16:20
  • 1
    @Remy It turns out that anonymous methods are in fact interfaces, and that implementation detail is widely used in this way: http://stackoverflow.com/a/39955320 – David Heffernan Feb 15 '17 at 16:32
  • @Dsm `TGpGraphicsPath` does not have a parameter in its constructor. – Remy Lebeau Feb 15 '17 at 16:37
  • Is this real code? `IWSmartPointer` is not declared to be an `interface`, so it is not valid to use in the `class()` declaration. And it does not make sense to assign an object pointer to a `reference to function`. Besides that, `Invoke()` is misnamed, as it simply returns the object pointer being managed, it does not actually invoke anything. I would rename it to `GetObject()` or similar, and then define a `property` to call it. – Remy Lebeau Feb 15 '17 at 16:39
  • @RemyLebeau All the constructors declared by `TGpGraphicsPath` have parameters – David Heffernan Feb 15 '17 at 16:51
  • @RemyLebeau Yes it is real code. It compiles. The compiler accepts an anon method type in lieu of an interface because of the implementation details that I mentioned. `Invoke` is not misnamed. That is the name of the single method of an anon method interface. It's the method that is invoked when you invoke the anon method. You are missing some important background details here, and until you catch up I'm not sure that such comments are very helpful. – David Heffernan Feb 15 '17 at 16:54
  • @DavidHeffernan I'm well aware of how anonymous method types are implemented under the hood, thank you very much. That is a *implementation detail*. An anonymous type *happens* to be implemented using an interface, but that does not mean it is *legal* to write a class that *implements* an anonymous type. A class can derive only from another class, and can implement only interfaces. That is the contract. The contract does not say an anonymous type *is* an interface, it just *happens* to use an interface in the current implementation, but they could decide to change that implementation someday. – Remy Lebeau Feb 15 '17 at 17:38
  • @remy I understand. I thought you were suggesting that the compiler would reject the code in the question. And I agree that using implementation details like this is setting yourself up for a fall. – David Heffernan Feb 15 '17 at 18:25
  • @DavidHeffernan: actually, I was expecting the compile to fail. – Remy Lebeau Feb 15 '17 at 22:15
  • Thanks to all for the very interesting replies. In fact, as I'm pretty new in Delphi, I based my smart pointer implementation on this article: https://www.adug.org.au/technical/language/smart-pointers/ I selected it because it was the simplest implementation I found. But I am still a little too beginner to judge the quality of this implementation, so I am forced to trust what I read :-) Perhaps later and with a better knowledge I will reconsider my smart pointer class, but for now it works as expected for me – Jean-Milost Reymond Feb 16 '17 at 13:00

1 Answers1

4

This is quite a complex trap that you have fallen into. When you write:

TGpGraphicsPath.Create

you might think that you are calling the parameterless constructor. But it is not so. You are in fact calling this constructor:

constructor Create(fillMode: TFillMode = FillModeAlternate); reintroduce; overload;      

You supply no argument, so the default value is provided by the compiler.

In your smart pointer class you write:

T.Create

This really is calling the parameterless constructor. But that is the constructor defined by TObject. When that constructor is used, the TGPGraphicsPath instance is not properly initialised.

If you are going to use the constructor generic constraint, you must also ensure that you always use a class that can be properly constructed with a parameterless constructor. Unfortunately for you TGPGraphicsPath does not fit the bill. Indeed there are a preponderance of such classes.

There's really not a whole lot that you can do here to avoid explicitly calling the constructor. It's pretty much impossible for your smart pointer class to work out which constructor to call, for this particular class.

My advice would be to steer away from the constructor generic constraint and force the consumer of the smart pointer class to explicitly instantiate the instance.

This is quite a common issue – I answered a similar question here less than a week ago: Why does a deserialized TDictionary not work correctly?

Community
  • 1
  • 1
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • A very nasty mistake being done by the original developer of generics/constraints and copying them from C# where ctors are automatically are inherited. Using a ctor constraint there makes sense and actually prevents using classes that don't have a parameterless ctor declared although their ancestor might have one. In Delphi the compiler sees the TObject ctor as soon as a child class has ctors that have overload added. But now that I wrote this I think this could be fixed by the compiler doing proper overload resolution here because it should not see the TObject ctor in these cases. – Stefan Glienke Feb 15 '17 at 19:46
  • @stefan I don't think the compiler can ever treat the one param constructor with default arg as a parameterless constructor, unless the underlying design is drastically changed. – David Heffernan Feb 15 '17 at 20:11
  • I don't see why the compiler can't simply look at the available constructors in the type and if a no-parameter constructor is found then look for a constructor that has all-defaulted parameters. The user code is the same either way, and this normally works fine outside of Generics, so the compiler should do the right thing regardless of whether the code is in a Generic or not. This change shouldn't break the `constructor` constraint if its definition is loosened a little to include any constructor that does not *require* input parameters by the user's code. – Remy Lebeau Feb 15 '17 at 22:21
  • @remy I don't think the current generics code could manage that. When calling methods it needs to know the arguments at the initial compilation phase I believe. And what you propose requires it to wait until each instantiation. – David Heffernan Feb 16 '17 at 05:33