Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 665
zig Avatar asked Dec 26 '17 14:12

zig


1 Answers

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:

  • An open array allows callers to pass a fixed array of unspecified length into a procedure. The compiler determines the length at compile-time and passes an additional hidden parameter (the High index) so the method can correctly determine the number of elements in the array.
  • A default parameter is simply syntactic sugar allowing the caller to omit defaults. The implementation is unaffected, but the compiler automatically passes omitted parameters as if the caller had passed the default.
  • The bug occurs when the procedure is marked for overload. The compiler seemingly "forgets" to pass the hidden High index, and passes the default in its place.

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:

  • The address of the open array in eax
  • The second argument (the default) in ecx
  • The High index of the array in edx

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.

like image 73
Disillusioned Avatar answered Oct 07 '22 02:10

Disillusioned