Using array of string with default parameter and overload procedure
Asked Answered
H

2

5

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.

Herwick answered 26/12, 2017 at 14:41 Comment(10)
Fwiw, your app gives 3, 3 on D7.Cottage
@MartynA, in that case I will have to live with that. I'll delete the question shortly. thanks for confirming.Herwick
@zig: Can't reproduce in XE2. I get 3 3 as wellLansing
@RemyLebeau, Should I delete the question or answer that it is indeed a bug in Delphi 5? I tried every compiler directive, the bug is consistent.Herwick
Add an answer...Impersonal
Out of interest, try changing the arg from boolean to integer and make the default value 2.Impersonal
@DavidHeffernan, It "works"! and if I increment the integer default, it will increment the Length(). How on earth did you figured that out???Herwick
Open arrays are passed by passing two args. The address of the first element and the index of the last element. A boolean default value of False has value 0 which is interpreted as high index 0, i.e. length 1. The bug appears to be that when an open array is followed by a default parameter, the high arg of the array is omitted and the compiler reads the next param as that high value. I had to ask you to check because I don't have D5 to hand.Impersonal
@DavidHeffernan, Can you post an answer an I will accept?Herwick
It's not really answer worthy. Your answer is fine. I was just showing off!!Impersonal
B
4

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.

Butta answered 28/12, 2017 at 14:3 Comment(1)
Thank you for a great answer!Herwick
H
4

This is clearly a bug in Delphi-5. it only happens if the following conditions are met:

  • The procedure is marked with overload

  • The array of string is the first parameter

  • No default parameter is passed to the procedure

Herwick answered 27/12, 2017 at 9:38 Comment(0)
B
4

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.

Butta answered 28/12, 2017 at 14:3 Comment(1)
Thank you for a great answer!Herwick

© 2022 - 2024 — McMap. All rights reserved.