Patterns for constructing reference-counted objects: Factory functions vs `as` operator

The construction of reference-counted object instances, which have automatic memory management, is a recurring topic.

The main problem with such instances is that they need to be assigned to an interface reference (explicit or implicit) to have a properly initialized reference counting mechanism and a properly managed lifetime.

There are many different coding patterns and scenarios around the construction and assignment of such instances. The simplest scenario is having an explicit interface reference to which we will assign the newly constructed object; in some, the compiler will lend us a hand and insert the appropriate code behind the scenes; and in some, it will do the opposite, due to either some compiler bugs or an ambiguity in the code which cannot be automatically resolved by the compiler.

In previous blog posts, I have previously covered some such scenarios which needed special attention from developers to avoid bugs:

And, of course, in my books: Delphi Memory Management in particular, but also in others, because memory management is often an inevitable topic when writing any Delphi code.

The purpose of this post is not to list all scenarios where we need to pay attention to how we are constructing such objects to avoid memory leaks and other bugs, but rather to explore the differences between possible solutions.

Besides the simplest scenario of having an explicit interface reference to which the reference-counted object will be assigned, there are two common construction patterns: One is using the as operator and typecasting the object as an interface, and another one is using factory functions.

as operator

Casting a newly created object instance as an interface with the as operator may look like a very simple and effective solution, which you can apply in any scenario where you need to trigger the reference counting mechanism.

var Foo := TFoo.Create as IFoo;

Factory function

A factory function requires a bit more work. You need to explicitly declare and write the function. Not to mention that you need to give it some name, and as we all know, naming is hard.

class function TFoo.New: IFoo; begin Result := TFoo.Create; end; var Foo := TFoo.New;

Comparison

If you take a brief look at the code, you may come to the (incorrect) conclusion that the as operator is a better choice. Why would you write additional code when you don't have to?

But, there are good reasons why using factory functions is actually a better solution.

  • Factory functions work on any kind of interface, while the as operator works only on interfaces with a declared GUID. This immediately makes using the as operator impossible for any generic interface.

  • Factory functions work better with code completion. After you type TFoo. the very next character can find the function and complete it. With the as operator you need to write the whole constructor first, then the as operator, then start typing the interface and hope that code completion will offer something usable instead of offering you a huge list of unrelated identifiers.

  • Once you have written your factory function, it can be used in all code, regardless of situation, which promotes code consistency and thus reduces the chance of accidental bugs. You can easily forget that particular code requires the as operator, as opposed to using factory functions everywhere.

  • More optimized code. Regardless of the particular compiler, its generated code, and how well-optimized it is or isn't, the as operator will always require more code and will run slightly longer than a factory function, as its backing code is more complex. Factory functions, just like a regular assignment to an interface reference will call the _CopyIntf procedure. Meanwhile, the as operator calls _IntfCast, which has more complex code and calls an additional function: QueryInterface.

Considering all of the above, factory functions are a clear winner, after all.

Organizing factory functions

When it comes to naming factory functions, there are various options like: Construct, Make, New... Which one you will use is really just a matter of personal style. I prefer New, as it is the shortest one.

There are a few different approaches to organizing factory functions.

The factory function can be a part of the class whose instances you are constructing:

TFoo = class(TInterfacedObject, IFoo) class function New: IFoo; static; inline; end;

If the class implements multiple interfaces, you can append the interface identifier to the function name:

TFooBar = class(TInterfacedObject, IFoo, IBar) class function NewFoo: IFoo; static; inline; class function NewBar: IBar; static; inline; end;

If the class is part of a larger framework, you can also combine all factory functions into a separate static class or record. If you like, you can also do that even if you will only put a single factory function in:

TSomeFactory = record // or class public class function NewFoo: IFoo; static; inline; class function NewBar: IBar; static; inline; ... end;

Example

The following code shows various situations and solutions for constructing object instances, so you can play with it and inspect the generated code and its behavior yourself. The ones with Leaky in the name are bad examples, which will create memory leaks and are for demonstration only.

The console output will show the reference count of the object instance after it is constructed: 0 means that reference counting is not initialized and the instance will leak, while 1 means that the reference count is successfully initialized and there will be no memory leaks.

Any number larger than 1 means that there are excess, implicit, references created. They should not leak unless there is a bug in the compiler, as any of those hidden references should be cleared by the compiler at the end of the scope in which they are created. However, even if there is no leak, unnecessary reference counting triggers will make such code run a tad slower.

You should also keep in mind that various compilers may generate different code and exhibit different behavior for the same Delphi construct. Those differences include compilers for different platforms in the same Delphi version.

program FactoryvsAsOperator; {$APPTYPE CONSOLE} {$O+} // turn on optimization {$W-} // generate stack frames only if necessary {$R *.res} uses System.SysUtils; type IFoo = interface ['{7692C88F-C478-4F1D-B786-8A4F2278BD2C}'] function GetRefCount: Integer; property RefCount: Integer read GetRefCount; end; TFoo = class(TInterfacedObject, IFoo) class function New: IFoo; static; inline; end; class function TFoo.New: IFoo; begin Result := TFoo.Create; end; procedure ConstParam(const Foo: IFoo); begin Writeln(Foo.RefCount); end; procedure TestLeakyCreate; begin Write('leaky constructor: '); var Foo := TFoo.Create; Writeln(Foo.RefCount); end; procedure TestCreate; begin Write('constructor: '); var Foo: IFoo := TFoo.Create; Writeln(Foo.RefCount); end; procedure TestAsOperator; begin Write('as operator: '); var Foo := TFoo.Create as IFoo; Writeln(Foo.RefCount); end; procedure TestAsOperatorInterface; begin Write('as operator and interface reference: '); var Foo: IFoo := TFoo.Create as IFoo; Writeln(Foo.RefCount); end; procedure TestFactory; begin Write('factory: '); var Foo := TFoo.New; Writeln(Foo.RefCount); end; procedure TestFactoryInterface; begin Write('factory and interface reference: '); var Foo: IFoo := TFoo.New; Writeln(Foo.RefCount); end; procedure TestExtraAsOperator; begin Write('factory and as operator: '); var Foo := TFoo.New as IFoo; Writeln(Foo.RefCount); end; procedure TestLeakyConstParam; begin Write('leaky const param: '); ConstParam(TFoo.Create); end; procedure TestAsOperatorConstParam; begin Write('as operator const param: '); ConstParam(TFoo.Create as IFoo); end; procedure TestFactoryConstParam; begin Write('factory const param: '); ConstParam(TFoo.New); end; begin ReportMemoryLeaksOnShutdown := True; TestLeakyCreate; TestCreate; TestAsOperator; TestAsOperatorInterface; TestFactory; TestFactoryInterface; TestExtraAsOperator; TestLeakyConstParam; TestAsOperatorConstParam; TestFactoryConstParam; end.

Comments

Popular posts from this blog

Celebrating 30 Years of Delphi With a New Book: Delphi Quality-Driven Development

Assigning result to a function from asynchronous code

Hello Old New Leaky Friend