delphidelphi-5

Using array of string with default parameter and overload procedure


I have this code, with a procedure that uses an overload and a default parameter:

program Project2;
{$APPTYPE CONSOLE}
uses SysUtils;

procedure Foo; overload; // not actually needed to reproduce
begin
end;

procedure Foo(const a: array of string; b: Boolean=False); overload;
begin
  Writeln(Length(a));
end;

begin
  Foo(['1', '2', '3']); // => 1 ???
  Foo(['1', '2', '3'], False); // => 3 OK
  Readln;
end.

The output is:

1
3

Note that the first call to Foo does not provide a default value. Why is this happening? is this issue only related to very old compilers?

This only happens if the overload key is used.

procedure Foo2(const a: array of string; b: Boolean=False);
begin
  Writeln(Length(a));
end;

Foo2(['1', '2', '3']);

Works fine.


Solution

  • Summary

    As you have discovered and David has helped clarify: this is a bug in Delphi 5 (and possibly a few other versions of that era). Under specific conditions the compiler fails to call the procedure correctly.

    It's essentially a clashing of 2 features:


    Workaround

    I'm sure you're already using the obvious workaround, but I include it for completeness. When I used to work in Delphi 5, we replaced all combinations of array of String and default with the following; (regardless of whether we were already using overload).

    procedure Foo(const a: array of string; b: Boolean); overload; {Remove the default}
    begin
      ...
    end;
    procedure Foo(const a: array of string); overload;
    begin
      Foo(a, False); {And pass the default value via overload}
    end;
    

    Details

    You can observe exactly how the compiler fails to call Foo correctly by debugging within the CPU window (Ctrl + Alt + C) and examining the assembler code.

    You should be able to deduce that the Foo procedure is compiled to expect:

    Note I used Integer default for a more distinctive default value.

    Case 1

    procedure Foo(const a: array of string; b: Integer = 7);
    ...
    Foo(['a', 'b', 'c']);
    {The last few lines of assembler for the above call}
    lea eax,[ebp-$18] {Load effective address of array}
    mov ecx,$00000007 {Implicitly set default value 7}
    mov edx,$00000002 {The hidden High value of the open array}
    call Foo
    

    Case 2

    procedure Foo(const a: array of string; b: Integer = 7); overload;
    ...
    Foo(['a', 'b', 'c']);
    
    lea eax,[ebp-$18]
    {The second parameter is now uninitialised!}
    mov edx,$00000007 {Instead the default is assigned to register for High(a)}
    call Foo
    

    Case 3

    procedure Foo(const a: array of string; b: Integer = 7); overload;
    ...
    Foo(['a', 'b', 'c'], 5);
    
    lea eax,[ebp-$18]
    mov ecx,$00000005 {The explicit argument for 2nd parameter}
    mov edx,$00000002 {The hidden parameter is again correctly assigned}
    call Foo
    

    Additional Observations

    1) As pointed out in case 2 above, when the bug manifests, ecx is left uninitialised. The following should demonstrate the effect:

    procedure Foo(const a: array of string; b: Integer = 2); overload;
    var
      I: Integer;
    begin
      for I := Low(a) to High(a) do Write(a[I]);
      Writeln(b);
    end;
    ...
    Foo(['a', 'b', 'c'], 23); {Will write abc23}
    Foo(['a', 'b', 'c']); {Will write abc, but the number probably won't be 2}
    

    2) The bug doesn't manifest with dynamic arrays. The length of a dynamic array is a part of its internal structure and hence cannot be forgotten.