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 theas
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 theas
operator you need to write the whole constructor first, then theas
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, theas
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
Post a Comment