Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it necessary to assign a default value to a variant returned from a Delphi function?

Gradually I've been using more variants - they can be very useful in certain places for carrying data types that are not known at compile time. One useful value is UnAssigned ('I've not got a value for you'). I think I discovered a long time ago that the function:

function DoSomething : variant;
begin
  If SomeBoolean then
    Result := 4.5
end;

appeared to be equivalent to:

function DoSomething : variant;
begin 
  If SomeBoolean then
    Result := 4.5
   else
   Result := Unassigned; // <<<<
end;

I presumed this reasoning that a variant has to be created dynamically and if SomeBoolean was FALSE, the compiler had created it but it was 'Unassigned' (<> nil?). To further encourage this thinking, the compiler reports no warning if you omit assigning Result.

Just now I've spotted nasty bug where my first example (where 'Result' is not explicity defaulted to 'nil') actually returned an 'old' value from somewhere else.

Should I ALWAYS assign Result (as I do when using prefefined types) when returing a variant?

like image 390
Brian Frost Avatar asked Jul 08 '11 09:07

Brian Frost


2 Answers

Yes, you always need to initialize the Result of a function, even if it's a managed type (like string and Variant). The compiler does generate some code to initialize the future return value of a Variant function for you (at least the Delphi 2010 compiler I used for testing purposes does) but the compiler doesn't guarantee your Result is initialized; This only makes testing more difficult, because you might run into a case where your Result was initialized, base your decisions on that, only to later discover your code is buggy because under certain circumstances the Result wasn't initialized.

From my investigation, I've noticed:

  • If your result is assigned to a global variable, your function is called with an initialized hidden temporary variable, creating the illusion that the Result is magically initialized.
  • If you make two assignments to the same global variable, you'll get two distinct hidden temporary variables, re-enforcing the illusion that Result's are initialized.
  • If you make two assignments to the same global variable but don't use the global variable between calls, the compiler only uses 1 hidden temporary, braking the previous rule!
  • If your variable is local to the calling procedure, no intermediary hidden local variable is used at all, so the Result isn't initialized.

Demonstration:

First, here's the proof that a function returning a Variant receives a var Result: Variant hidden parameter. The following two compile to the exact same assembler, shown below:

procedure RetVarProc(var V:Variant);
begin
  V := 1;
end;

function RetVarFunc: Variant;
begin
  Result := 1;
end;

// Generated assembler:
push ebx // needs to be saved
mov ebx, eax // EAX contains the address of the return Variant, copies that to EBX
mov eax, ebx // ... not a very smart compiler
mov edx, $00000001
mov cl, $01
call @VarFromInt
pop ebx
ret

Next, it's interesting to see how the call for the two is set up by the complier. Here's what happens for a call to a procedure that has a var X:Variant parameter:

procedure Test;
var X: Variant;
begin
  ProcThatTakesOneVarParameter(X);
end;

// compiles to:
lea eax, [ebp - $10]; // EAX gets the address of the local variable X
call ProcThatTakesOneVarParameter

If we make that "X" a global variable, and we call the function returning a Variant, we get this code:

var X: Variant;

procedure Test;
begin
  X := FuncReturningVar;
end;

// compiles to:
lea eax, [ebp-$10] // EAX gets the address of a HIDDEN local variable.
call FuncReturningVar // Calls our function with the local variable as parameter
lea edx, [ebp-$10] // EDX gets the address of the same HIDDEN local variable.
mov eax, $00123445 // EAX is loaded with the address of the global variable X
call @VarCopy // This moves the result of FuncReturningVar into the global variable X

If you look at the prologue of this function you'll notice the local variable that's used as a temporary parameter for the call to FuncReturningVar is initialized to ZERO. If the function doesn't contain any Result := statements, X would be "Uninitialized". If we call the function again, a DIFFERENT temporary and hidden variable is used! Here's a bit of sample code to see that:

var X: Variant; // global variable
procedure Test;
begin
  X := FuncReturningVar;
  WriteLn(X); // Make sure we use "X"
  X := FuncReturningVar;
  WriteLn(X); // Again, make sure we use "X"
end;

// compiles to:
lea eax, [ebp-$10] // first local temporary
call FuncReturningVar
lea edx, [ebp-$10]
mov eax, $00123456
call @VarCopy
// [call to WriteLn using the actual address of X removed]
lea eax, [ebp-$20] // a DIFFERENT local temporary, again, initialized to Unassigned
call FuncReturningVar
// [ same as before, removed for brevity ]

When looking at that code, you'd think the "Result" of a function returning Variant is allways initialized to Unassigned by the calling party. Not true. If in the previous test we make the "X" variable a LOCAL variable (not global), the compiler no longer uses the two separate local temporary variables. So we've got two separate cases where the compiler generates different code. In other words, don't make any assumptions, always assign Result.

My guess about the different behavior: If the Variant variable can be accessed outside the current scope, as a global variable (or class field for that matter) would, the compiler generates code that uses the thread-safe @VarCopy function. If the variable is local to the function there are no multi-threading issues so the compiler can take the liberty to make direct assignments (no-longer calling @VarCopy).

like image 63
Cosmin Prund Avatar answered Nov 15 '22 14:11

Cosmin Prund


Should I ALWAYS assign Result (as I do when using prefefined types) when returing a variant?

Yes.

Test this:

function DoSomething(SomeBoolean: Boolean) : variant;
begin
    if SomeBoolean then
        Result := 1
end;

Use the function like this:

var
    xx: Variant;
begin
    xx := DoSomething(True);
    if xx <> Unassigned then
        ShowMessage('Assigned');

    xx := DoSomething(False);
    if xx <> Unassigned then
        ShowMessage('Assigned');
end;

xx will still be assigned after the second call to DoSomething.

Change the function to this:

function DoSomething(SomeBoolean: Boolean) : variant;
begin
    Result := Unassigned;
    if SomeBoolean then
        Result := 1
end;

And xx is not assigned after the second call to DoSomething.

like image 21
Mikael Eriksson Avatar answered Nov 15 '22 15:11

Mikael Eriksson