Inline Variables Type Inference And Reference Counted Classes

There are two (OK, three) features I have been eagerly waiting for in Delphi.
  1. The ability to have multiple class helpers (aka extension methods) in scope
  2. Inline variables and (3.) type inference
While I will have to wait some more for the former, Delphi Rio introduced the latter, making me a very happy camper.

But it is not all perfect. Type inference does not work on reference counted classes. Or rather, it does work, but it breaks reference counting.

For example, this inline variable declaration:
var Ref := TInterfacedObject.Create;
will be translated by the compiler into:
var Ref: TInterfacedObject := TInterfacedObject.Create;

In the above code, the compiler correctly infers the type - TInterfacedObject, but by doing so, it stores a reference counted object in an object reference. And as we all know, this kind of code breaks the reference counting mechanism. The famous "Don't mix objects and interfaces" rule.

To properly initialize reference counting, you have to explicitly declare an interface type or use type casting when declaring such an inline variable:
var Ref: IInterface := TInterfacedObject.Create; var Ref := TInterfacedObject.Create as IInterface;

To be fair, this is not the fault of type inference per-se, but rather one of those places where the non-ARC compiler exhibits the full complexity of its memory management model(s). The ARC compiler does not suffer from this, since it has unified memory management (ARC) for both object and interface references.

Problem with the above wrong type inference is that it represents a rather subtle error. When you explicitly declare a variable type, mistakes like storing a reference counted instance in an object reference are way more obvious.

Of course, this is in no way an argument against using inline variables, nor relying on type inference. Just something to keep in mind when you are using them.

On the bright side, this applies only for code that infers types from constructor calls. If you have a function returning interface, then type inference will work properly.

Comments

  1. "but by doing so, it stores a reference counted object in an object reference.". Well no, it actually stores an object reference in an object reference variable. And it is not reference counted at all. It is an object like any other, even if it contains a mechanism with which you can count references.

    If you *want* to turn that into an interface and use it as such, you must indeed tell the compiler.

    ReplyDelete
    Replies
    1. It stores object with enabled reference counting mechanism into object reference, breaking reference counting mechanism and any subsequent code that triggers reference counting mechanism will prematurely release that object.

      You never *want* to write such fragile code.

      Delete
    2. In what way does it break the reference counting mechanism?

      Delete
    3. Rules are explained in Don't Mix Objects and Interfaces

      What happens behind the scenes is: object instance upon construction has reference count 0. Working reference counting mechanism requires initial reference count 1.

      When you assign newly constructed object instance to interface reference, compiler will insert call to _AddRef and increase reference count to 1. When that and all other strong references to the object go out of scope, reference count will drop to 0 and at that moment object will be destroyed.

      If object instance has reference count 0 - and you call code that triggers reference counting mechanism - for instance pass it as non const parameter - compiler will insert _AddRef (1) at procedure entry point, and then call _Release at the end - this call will decrease reference count to 0 automatically killing the object.

      Of course, you can store reference counted object instance in object reference, but then you have to manually call _AddRef after construction and _Release after you are done.

      Delete
    4. But that is only if you want to use these objects as interfaces. I don't quite see the problem. If you assign such an object to an old style variable, i.e. a non-inline, non-type-inferred instance reference, you get the same behaviour. So you must indeed do what you wrote, but that is, IMO, self-evident. Nothing wrong with it and nothing weird about it.

      You wrote: "but not all is perfect". Well, probably not, but this is not the problem.

      Delete
    5. If you assign such object to an old style variable declared as TInterfacedObject such code would be equally broken and error prone. No difference there.

      Point is, when class implements reference counting you are not in position to decide how you want to use it. It is meant to be used with working reference counting. Anything else is just plain BAD code and practice.

      Any non trivial code CAN trigger reference counting mechanism and prematurely destroy such object. While it may work if there is no code that triggers reference counting, problem with such code is that any subsequent code changes far away from your three lines of code where you construct object, can change behavior triggering reference counting mechanism and causing errors.

      If you like having such fragile code in your code base, be my guest. But it is not something I would recommend.

      Delete
    6. "Point is, when class implements reference counting you are not in position to decide how you want to use it. It is meant to be used with working reference counting. Anything else is just plain BAD code and practice. "

      I thoroughly disagree. That a class may be implementing one or more interfaces does not mean that these are the main purpose of the class. An interface like IPrintable or IPersistable would not necessarily always be needed.

      If you consider some classes ONLY as implementations of a certain interface, then you may be right.

      If you see an interface as an aspect of a class that may be important in one situation but not in another, then you are wrong. These classes are not necessarily meant to be used as interfaces, and it is probably not their main purpose. That is not bad design, that is actually normal design (I don't believe in the credo that everything must be "designed to the interface" where some people mean real interface types instead of the concept of an interface).

      But you must know that, because of interface-ARC, you must cast to an interface before it is used. And you must know how to handle the interface after use. That is not more fragile than writing classes just to implement a single interface.

      Delete
    7. Apples vs oranges. You are focusing on class purpose. It is completely irrelevant from memory management perspective. It does not matter whether it implements single interface or multiple ones.

      What matters is that if class implements and relies on ARC to manage its memory, then that mechanism MUST be properly initialized. If it is not, your object instance may vanish in thin air while you are still using it. Period.

      If some particular code at some point works even with improper initialization, it is merely a lucky coincidence. Like I said, any reference counting trigger will nuke the object, simple code like passing as value parameter, assigning to weak interface reference, using Supports function....

      Delete

Post a Comment

Popular posts from this blog

Coming in Delphi 12: Disabled Floating-Point Exceptions

Assigning result to a function from asynchronous code

Beware of loops and tasks