After
talking about const parameters last time today we take a closer look at out parameters and how they differ from var parameters.
I like the concept of out parameters as they clearly state that only the value that comes back matters and not the value that goes in.
However, the Delphi implementation of out parameters has some aspects to it that can cause a decision against them and rather choose a var parameter.
As last time we will look at some assembler code that shows what those differences are. Out parameters like var parameters pass a reference to the value rather than the value itself.
The code for this article will be this:
{$APPTYPE CONSOLE}
{$O+,W-}
type
TTestType = ...;
procedure C(z: TTestType);
begin
end;
procedure A(var y: TTestType);
begin
y := Default(TTestType);
end;
procedure B(out y: TTestType);
begin
y := Default(TTestType);
end;
procedure Main;
var
x: TTestType;
begin
A(x);
B(x);
end;
Like last time, we will inspect the code for different types for TTestType and see how it differs.
Let's start with Integer - the code we will see for the calls and inside A and B are as follows:
OutParams.dpr.24: A(x);
00408FA5 8BC4 mov eax,esp
00408FA7 E8E8FFFFFF call A
OutParams.dpr.25: B(x);
00408FAC 8BC4 mov eax,esp
00408FAE E8E9FFFFFF call B
OutParams.dpr.12: y := Default(TTestType);
00408F94 33D2 xor edx,edx
00408F96 8910 mov [eax],edx
OutParams.dpr.13: end;
00408F98 C3 ret
OutParams.dpr.17: y := Default(TTestType);
00408F9C 33D2 xor edx,edx
00408F9E 8910 mov [eax],edx
OutParams.dpr.18: end;
00408FA0 C3 ret
Nothing really fancy here - esp is the address of the local variable x. The (
unfortunately still undocumented) intrinsic function Default() takes care of setting the parameter to the default value of the given type - so in this case 0. The code for the var parameter will look exactly the same.
For other types, the code will look similar but something interesting happens when we use a managed type such as string. Take a look at how this changes the code being generated:
OutParams.dpr.24: A(x);
00408FCB 8D45FC lea eax,[ebp-$04]
00408FCE E8C1FFFFFF call A
OutParams.dpr.25: B(x);
00408FD3 8D45FC lea eax,[ebp-$04]
00408FD6 E8F1CEFFFF call @UStrClr
00408FDB E8C0FFFFFF call B
On the caller side, we now see a call to System._UStrClr which the compiler inserted here to ensure that x will be empty before being passed to A. Out parameters are always initialized before they are passed, unlike var parameters.
When we will take a look at the code inside A we will see nothing unusual except a lack of optimization. eax could directly be passed to System._UStrClr - and that is not a result of using Default() but also happens when assigning '' to y:
OutParams.dpr.11: begin
00408F94 53 push ebx
00408F95 8BD8 mov ebx,eax
OutParams.dpr.12: y := Default(TTestType);
00408F97 8BC3 mov eax,ebx
00408F99 E82ECFFFFF call @UStrClr
OutParams.dpr.13: end;
00408F9E 5B pop ebx
00408F9F C3 ret
The interesting difference however will be obvious when we will look into B:
OutParams.dpr.16: begin
00408FA0 55 push ebp
00408FA1 8BEC mov ebp,esp
00408FA3 53 push ebx
00408FA4 8BD8 mov ebx,eax
00408FA6 85DB test ebx,ebx
00408FA8 7404 jz $00408fae
00408FAA 33C0 xor eax,eax
00408FAC 8903 mov [ebx],eax
OutParams.dpr.17: y := Default(TTestType);
00408FAE 8BC3 mov eax,ebx
00408FB0 E817CFFFFF call @UStrClr
OutParams.dpr.18: end;
00408FB5 5B pop ebx
00408FB6 5D pop ebp
00408FB7 C3 ret
What is going on here? The first two instructions are setting up a frame pointer which is usual but would not be necessary in this case - especially since we turned off that option. The following lines basically ensure that our y parameter really is empty - again with a lack of optimization.
Why is this happening? The caller side ensured that y is empty. This code is for C++Builder compatibility! C++ does not have the concept of out parameter but just by reference parameter which equals the var parameter in Delphi. And because of that when C++ would call such a routine the value would not have been initialized. Unfortunately, at least to my knowledge, there is no option to turn this off because our code will never be called from C++. We have built an executable here in Delphi; our function is not exported nor did we make a DLL.
When using out parameters you pay a price for some most likely unused feature as most if not all Delphi code you ever write will never be called from any C++ code.
The impact of out parameters is so significant in some cases that in 10.4 some of them were changed to var in the RTL - take a look at most parameters of TValue in the unit System.Rtti. Because for records this overhead can be even bigger and even more when they are passed multiple levels of out parameters because every call and routine again produces this completely unnecessary overhead.
The entire concept of out parameters to me looks completely unfinished - let's for a second take a look at some C# code which also has out parameters
static void B(out int y)
{
WriteLine(y);
}
This code will raise two errors:
Error CS0269 Use of unassigned out parameter 'y'
Error CS0177 The out parameter 'y' must be assigned to before control leaves the current method
And the compiler is right - since the parameter is out the value going in actually is undefined behavior - and if the compiler ensures that it cannot be used then it also does not need to do any initialization ensuring any value. That means no unnecessary work to do and no inconsistent behavior - remember unmanaged data types don't get that extra treatment, whatever value is in an int variable being passed is still in there when being passed to an out parameter.
Second, the compiler ensures setting a value to the out parameter ensuring it is not undefined when returning from this routine. Would an exception occur during the invocation of that routine and before the out parameter was set then upon returning and catching the exception the variable would still contain the value it had before - a totally sane and understandable behavior.
As for me - I will be more careful where I use out parameters and in fact, during the refactoring of the collections in Spring4D, I replaced most out parameters with var parameters. Since even without the compiler enforcing this, all code paths inside those methods eventually set the value of the parameter it will be no change in behavior for you but avoid this completely insane overhead.