I\'m trying to build an custom comparer which allows the assignment of the comparison function to an internal field. In order to ease the creation of the comparer, I tried t
I don't think that this is a bug. Critically, you've defined TConstFunc
as an anonymous method type. These are managed, reference counted, very special types that are quite different from regular object methods. By compiler magic they are usually assignment compatible, but with several important caveats. Consider the more concise :
program Project1;
{$APPTYPE CONSOLE}
type
TFoo = reference to procedure;
TDemo = class
private
FFoo : TFoo;
procedure Foo;
public
class function Construct(): TDemo;
end;
procedure TDemo.Foo;
begin
WriteLn('foo');
end;
class function TDemo.Construct: TDemo;
begin
result := TDemo.Create();
result.FFoo := result.foo;
end;
end.
This also produces the same compiler error (E2555). Because the member method is a procedure of object
(object method) type, and you are assigning it to a reference to procedure
(anonymous method) type, this is equivalent to (and I suspect that the compiler is expanding this as) :
class function TDemo.Construct: TDemo;
begin
result := TDemo.Create();
result.FFoo := procedure
begin
result.foo;
end;
end;
The compiler cannot assign the method reference directly (since they are of different types), and therefore (I guess) has to wrap it in an anonymous method which is implicitly requiring capture of the result
variable. Function return values cannot be captured by anonymous methods, however - only local variables can.
In your case (or, indeed, for any function
type), the equivalent cannot even be expressed due to the anonymous wrapper hiding the result
variable, but we can imagine the same in theory as:
class function TDemo.Construct: TDemo;
begin
Result := TDemo.Create();
Result.FVar := function(const L, R : string) : integer
begin
result := result.CompareInternal(L,R); // ** can't do this
end;
end;
As David has shown, introducing a local variable (which can be captured) is one correct solution. Alternatively, if you don't need the TConstFunc
type to be anonymous, you can simply declare it as a regular object method :
TConstFunc<T1, T2, TResult> = function(const Arg1: T1; const Arg2: T2): TResult of object;
Another example where attempting to capture result
fails :
program Project1;
{$APPTYPE CONSOLE}
type
TBar = reference to procedure;
TDemo = class
private
FFoo : Integer;
FBar : TBar;
public
class function Construct(): TDemo;
end;
class function TDemo.Construct: TDemo;
begin
result := TDemo.Create();
result.FFoo := 1;
result.FBar := procedure
begin
WriteLn(result.FFoo);
end;
end;
end.
The fundamental reason why this does not work is because a method's return value is effectively a var
parameter and the anonymous closure captures variables, not values. This is a critical point. Similarly, this is also not allowed :
program Project1;
{$APPTYPE CONSOLE}
type
TFoo = reference to procedure;
TDemo = class
private
FFoo : TFoo;
procedure Bar(var x : integer);
end;
procedure TDemo.Bar(var x: Integer);
begin
FFoo := procedure
begin
WriteLn(x);
end;
end;
begin
end.
[dcc32 Error] Project1.dpr(18): E2555 Cannot capture symbol 'x'
In the case of a reference type, as in the original example, you really are only interested in capturing the value of the reference and not the variable that contains it. This does not make it syntactically equivalent and it would not be proper for the compiler to create a new variable for you for this purpose.
We could rewrite the above as this, introducing a variable :
procedure TDemo.Bar(var x: Integer);
var
y : integer;
begin
y := x;
FFoo := procedure
begin
WriteLn(y);
end;
end;
And this is allowed, but the expected behaviour would be very different. In the case of capturing x
(not allowed), we would expect that FFoo
would always write the current value of whatever variable was passed in as argument x
to Bar
, regardless of where or when it may have been changed in the interim. We would also expect that the closure would keep the variable alive even after it fell out of whatever scope created it.
In the latter case, however, we expect FFoo
to output the value of y
, which is the value of the variable x
as it was the last time Bar
was called.
Returning to the first example, consider this :
program Project1;
{$APPTYPE CONSOLE}
type
TFoo = reference to procedure;
TDemo = class
private
FFoo : TFoo;
FBar : string;
procedure Foo;
public
class function Construct(): TDemo;
end;
procedure TDemo.Foo;
begin
WriteLn('foo' + FBar);
end;
class function TDemo.Construct: TDemo;
var
LDemo : TDemo;
begin
result := TDemo.Create();
LDemo := result;
LDemo.FBar := 'bar';
result.FFoo := LDemo.foo;
LDemo := nil;
result.FFoo(); // **access violation
end;
var
LDemo:TDemo;
begin
LDemo := TDemo.Construct;
end.
Here it is clear with :
result.FFoo := LDemo.foo;
that we have not assigned a normal reference to the method foo
beloning to the instance of TDemo
stored in LDemo
, but have actually captured the variable LDemo
itself, not the value it contained at the time. Setting LDemo
to nil
afterwards naturally produces an access violation, even thought the object instance it referred to when the assignment was made is still alive.
This is radically different behaviour than if we simply defined TFoo
as a procedure of object
instead of a reference to procedure
. Had we done that instead, the above code works as one might naively expect (output foobar
to the console).
This is not a full fledged answer, rather notes to David's answer and to the topicstarter's question.
Using answer mode for posting source snippets.
class function TDemo.Construct: TDemo;
begin
Result := TDemo.Create();
Result.FVar := Result.CompareInternal;
end;
class function TDemo.Construct: TDemo;
var
Demo: TDemo;
begin
Demo := TDemo.Create();
Demo.FVar := Demo.CompareInternal;
Result := Demo;
end;
Those both snippets use the same template:
Sure, the p.2 here is just one single line, still
So I think we should assume that p.2 has risk of runtime error, risk of exception thrown. Then it is a textbook memory leak. The local function still holds the memory management responsibilities, since it did not passed the result outside. But it also does not fulfill the required cleanup.
From my perspective the correct pattern - and the one giving one more incentive to using a dedicated local variable than mere Result/Result
compiler confusion - should be
class function TDemo.Construct: TDemo;
var
Demo: TDemo;
begin
Demo := TDemo.Create(); // stage 1: creating an object
try // stage 1: accepting M/M responsibilities
Demo.FVar := Demo.CompareInternal; // stage 2: tuning and facing
// Demo.xxx := yyy; // ...potential risks of exceptions
// Demo.Connect(zzz); etc
Result := Demo; // stage 3: passing the object outside
Demo := nil; // stage 3: abandoning M/M responsibilities
// function exit should follow this line immediately, without other fault-risky statements
finally
Demo.Free; // proceeding with M/M in case of faults in stage 2
end;
end; // stage 3: passing the object outside - immediately after the assignments!
UPD: ventiseis: And as a side node: I would try to instantiate the configurated comparer TDemo only once. The comparison function should be a stateless function
TDemo = class(TComparer<string>)
private
class var FVar: TConstFunc<string, string, Integer>;
// function CompareInternal(const L, R: string): Integer; STATIC; // also possible
class constructor InitComp;
...
end;
// would only be called once, if the class is actually used somewhere in the project
class constructor TDemo.InitComp;
begin
FVar := function(const L, R: string): Integer
begin
Result := StrToInt(R) - StrToInt(L)
end
end;
The compiler error on my English Delphi reads:
[dcc32 Error] E2555 Cannot capture symbol 'Result'
This is due to a defective design. There's no reason for any variable capture to be taking place here at all. The right hand side of the assignment is an instance method rather than an anonymous method. But the compiler handles that by wrapping the method in an anonymous method. The compiler translates
Result.FVar := Result.CompareInternal;
to
Result.FVar :=
function(const Arg1, Arg2: string): Integer
begin
InnerResult := OuterResult.CompareInternal(Arg1, Arg2);
end;
Leaving aside the confusion over the two separate result variables, the compiler rejects this because the outer result variable is not a local, it's a var
parameter. And so cannot be captured.
But the whole design is wrong in my view. There's no need for any variable capture. When you write Result.CompareInternal
you intend to refer to a normal of object
method. With a better design, the compiler would allow this assignment without creating an anonymous method.
You can work around the problem like this:
class function TDemo.Construct: TDemo;
var
Demo: TDemo;
begin
Demo := TDemo.Create();
Demo.FVar := Demo.CompareInternal;
Result := Demo;
end;
Here the local variable Demo
can be captured.
Or as I would suggest, like this:
program ConsoleDemo1;
{$APPTYPE CONSOLE}
uses
Generics.Defaults,
System.SysUtils;
type
TConstFunc<T1, T2, TResult> = reference to function(const Arg1: T1;
const Arg2: T2): TResult;
TDemo = class(TComparer<string>)
private
FVar: TConstFunc<string, string, Integer>;
function CompareInternal(const L, R: string): Integer;
public
constructor Create;
function Compare(const L, R: string): Integer; override;
end;
constructor TDemo.Create;
begin
inherited;
FVar := CompareInternal;
end;
function TDemo.Compare(const L, R: string): Integer;
begin
Result := FVar(L, R);
end;
function TDemo.CompareInternal(const L, R: string): Integer;
begin
Result := AnsiCompareStr(L, R);
end;
end.