Open array parameters and array of const
Rudy Velthuis
Rudy Velthuis
[SHOWTOGROUPS=4,20]
This article describes the syntax and use of open array parameters, and the use of the “array of const” parameter type. It also describes the internals of these two similar types of parameters, discusses lifetime issues, and gives a code solution for these issues. It has a short discusson on the confusion between open arrays and dynamic arrays, and between Variant arrays and variant open arrays.
Open array parameters
Open array parameters are special parameters which allow you to write procedures or functions (I will use the word routines, if I mean both) that can act on any array of the same base type, regardless of its size. To declare an open array parameter, you use a syntax like this:
You can call this procedure with any one-dimensional array of Integers, so you can call it with an array[0..1] of Integer as well as with an array[42..937] of Integer, or even a dynamic type array of Integer.
The code also demonstrates how you can determine the size of the array in the routine. Delphi knows the pseudo-functions Low and High. They are not real functions, they are just syntactic items in Delphi that take the form of a function, but actually rely on the compiler to substitute them for code. Low gives the lower bound of an array, and High the upper bound. You can also use Length, which returns the number of elements of the array.
But if you call the code with an array that is not zero-based, like for instance in the following (nonsense) example,
you will see that the output is like this:
That is because inside the procedure or function, the array is always seen as a zero based array. So for an open array parameter, Low is always 0, and High is adjusted accordingly (note that this is not necessarily true for other uses of High and Low, i.e. not on open array parameters). For open arrays, Length is always High + 1.
Slice
If you don’t want to use an entire array, but only a part of it, you can do that using the Slice pseudo-function. It is only allowed where an open array parameter is declared. It is used in this fashion:
That will only display the first 6 values of the array, not all 12.
Note that with Slice, you can fool the compiler. If range checking is not on (in the Project options, or in the form of {$R-} or {$RANGECHECKS OFF}), and you give a value for the slice length that is higher than the actual length, this is not detected by the compiler. So you are telling the called function it can access elements it should actually not access.
This is not a problem with static arrays and a constant size. The compiler checks those at compile time and will issue an error. But if the size is a variable or the array is dynamic, the compiler can’t check this at compile time. Then it can happen that you do:
If ListAllIntegers really tries to access index 12 (remember, the index of an open array parameter is zero-based, so High should be 11) or higher, it is accessing beyond the bounds of the array. This is undefined behaviour. It can result in a crash, or other bad things, depending on what is actually accessed.
Without Slice, the compiler passes the right values for High, so this cannot happen. So be careful with this pseudo-function.
Internals
But how does that work; how can the function know the size of the array? It is quite simple. An open array parameter is actually a combination of two parameters, a pointer, which contains the address of the start of the array, and an integer, which contains the High value, adjusted for a zero base. So in fact the real parameter list of the procedure is something like this:
Each time you pass an array to an open array parameter, the compiler, which knows the size of the array, will pass its address and its adjusted High value to the procedure or function. For arrays of a static size, like array[7..9] of Integer, it uses the declared size to pass the High value; for dynamic arrays, it compiles code to get the High value of the array at runtime.
Usually, you can pass open arrays as const parameters. Open array parameters that are not passed as const will entirely be copied into local storage of the routine. The array is simply passed by reference, but if it is not declared const, the hidden start code of the routine will allocate room on the stack and copy the entire array to that local storage, using the reference as source address. For large arrays, this can be very inefficient. So if you don’t need to modify items in the array locally, make the open array parameter const.
Open array constructors
Sometimes you don’t want to declare and fill an array just so you can use it with an open array parameter. Luckily, Delphi allows you to declare open arrays on the spot, using the so called open array constructor syntax, which uses [ and ] to define the array. The above example with the NonZero array could also have been written like this:
Here, the array is defined on the spot as [17, 325, 11].
Lifetime
An array created by an open array constructor is only valid as long as the function or procedure runs and is discarded right afterward. That is one reason why you can’t pass such an array to a var open array parameter.
Internally, the open array constructor simply makes room on the stack (the local variable frame) and puts copies of the values there. In other words, it creates an ad hoc array on the stack. Then it calls the function passing a pointer to the first value on the stack and the High value of that array. After the function call, the stack room is reclaimed, so the constructed array does not exist anymore.
Managed (reference counted) types like strings, interfaces, etc. are copied raw to the stack array and not finalized after the call, i.e. no reference counting is done. This is equivalent to passing a const parameter.
Confusion
Although the syntax is unfortunately very similar, an open array parameter should not be confused with a Delphi dynamic array. A dynamic array is an array that is maintained by Delphi, and of which you can change the size using SetLength. It is declared like:
Unfortunately, this looks a lot like the syntax used for open array parameters. But they are not the same. An open array parameter will accept dynamic arrays like array of Month, but also static arrays like array[0..11] of Month. So in a function with an open array parameter, you can’t call SetLength on the parameter. If you really only want to pass dynamic arrays, you’ll have to declare them separately, and use the type name as parameter type.
Procedure AllKinds will accept static arrays as well as dynamic arrays, so SetLength can’t be used, since static arrays can’t be reallocated. Procedure OnlyDyn will only accept dynamic arrays, so you can use SetLength here (this will however use a copy, and not change the original array; if you want to change the length of the original array, use var Arr: TMonthArray in the declaration).
Note: You should not forget that in Delphi, parameters can ony be declared with type specifications, and not with type declarations. So the following formal parameters, which would be type declarations, are not possible:
You’ll have to declare a type first, and use the specifications as parameter type:
That is why array of Something in a parameter list can’t be a type declaration for a dynamic array either. It is always an open array declaration.
The latest Delphi compilers allow you to define dynamic array constants, so you can do:
You should not confuse these, despite the similar syntax, with open array constructors. If you code:
then you are not passing a dynamic array constant, you are using an open array constructor.
In versions of Delphi that know generics, you will see the type TArray<T> being used. It is declared as
This is a generic type, and only useful if T is actually replaced by a concrete type, for instance
The construct is being used more and more, because generic types can circumvent the general rule that type compatibility is not dependent on the form of the declaration, but on where the type is defined. So not all array of Month are assignment compatible with each other, although they have exactly the same form. But all TArray<Month> are assignment compatible, even if they are declared on the spot. This is an exception to the rule I explain in the text box above. TArray<T> can be used as parameter and even as return type, so you can have declarations like the following.
Assembler
To use an open array from assembler, you must remember that an open array is in fact a combination of two parameters. The first parameter is a pointer to the start of the array, the second the adjusted High value. Generally, open arrays are passed as const or var. In all cases, the array is passed by reference.
Here is a simple example of a function that sums all integers in an array:
The example above uses the usual register calling convention. If your function or procedure has a different calling convention, you may have to address the open array differently, but still as a combination of an address and a High value. If the procedure or function is in fact an instance method, then the first parameter (here: EAX) will be its implicit Self parameter, and Data will be passed in the next two parameters, in this case EDX and ECX.
[/SHOWTOGROUPS]
This article describes the syntax and use of open array parameters, and the use of the “array of const” parameter type. It also describes the internals of these two similar types of parameters, discusses lifetime issues, and gives a code solution for these issues. It has a short discusson on the confusion between open arrays and dynamic arrays, and between Variant arrays and variant open arrays.
Open array parameters
Open array parameters are special parameters which allow you to write procedures or functions (I will use the word routines, if I mean both) that can act on any array of the same base type, regardless of its size. To declare an open array parameter, you use a syntax like this:
Код:
procedure ListAllIntegers(const AnArray: array of Integer);
var
I: Integer;
begin
for I := Low(AnArray) to High(AnArray) do
WriteLn('Integer at index ', I, ' is ', AnArray[I]);
end;
The code also demonstrates how you can determine the size of the array in the routine. Delphi knows the pseudo-functions Low and High. They are not real functions, they are just syntactic items in Delphi that take the form of a function, but actually rely on the compiler to substitute them for code. Low gives the lower bound of an array, and High the upper bound. You can also use Length, which returns the number of elements of the array.
But if you call the code with an array that is not zero-based, like for instance in the following (nonsense) example,
Код:
var
NonZero: array[7..9] of Integer;
begin
NonZero[7] := 17;
NonZero[8] := 325;
NonZero[9] := 11;
ListAllIntegers(NonZero);
end.
Код:
Integer at index 0 is 17
Integer at index 1 is 325
Integer at index 2 is 11
Slice
If you don’t want to use an entire array, but only a part of it, you can do that using the Slice pseudo-function. It is only allowed where an open array parameter is declared. It is used in this fashion:
Код:
const
Months: array[1..12] of Integer = (31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31);
begin
ListAllIntegers(Slice(Months, 6));
end;
Note that with Slice, you can fool the compiler. If range checking is not on (in the Project options, or in the form of {$R-} or {$RANGECHECKS OFF}), and you give a value for the slice length that is higher than the actual length, this is not detected by the compiler. So you are telling the called function it can access elements it should actually not access.
This is not a problem with static arrays and a constant size. The compiler checks those at compile time and will issue an error. But if the size is a variable or the array is dynamic, the compiler can’t check this at compile time. Then it can happen that you do:
Код:
var
N: Integer;
begin
N := 18;
ListAllIntegers(Slice(Months, N));
end;
Without Slice, the compiler passes the right values for High, so this cannot happen. So be careful with this pseudo-function.
Internals
But how does that work; how can the function know the size of the array? It is quite simple. An open array parameter is actually a combination of two parameters, a pointer, which contains the address of the start of the array, and an integer, which contains the High value, adjusted for a zero base. So in fact the real parameter list of the procedure is something like this:
Код:
procedure ListAllIntegers(const AnArray: Pointer; High: Integer);
Usually, you can pass open arrays as const parameters. Open array parameters that are not passed as const will entirely be copied into local storage of the routine. The array is simply passed by reference, but if it is not declared const, the hidden start code of the routine will allocate room on the stack and copy the entire array to that local storage, using the reference as source address. For large arrays, this can be very inefficient. So if you don’t need to modify items in the array locally, make the open array parameter const.
Open array constructors
Sometimes you don’t want to declare and fill an array just so you can use it with an open array parameter. Luckily, Delphi allows you to declare open arrays on the spot, using the so called open array constructor syntax, which uses [ and ] to define the array. The above example with the NonZero array could also have been written like this:
Код:
ListAllIntegers([17, 325, 11]);
Lifetime
An array created by an open array constructor is only valid as long as the function or procedure runs and is discarded right afterward. That is one reason why you can’t pass such an array to a var open array parameter.
Internally, the open array constructor simply makes room on the stack (the local variable frame) and puts copies of the values there. In other words, it creates an ad hoc array on the stack. Then it calls the function passing a pointer to the first value on the stack and the High value of that array. After the function call, the stack room is reclaimed, so the constructed array does not exist anymore.
Managed (reference counted) types like strings, interfaces, etc. are copied raw to the stack array and not finalized after the call, i.e. no reference counting is done. This is equivalent to passing a const parameter.
Confusion
Although the syntax is unfortunately very similar, an open array parameter should not be confused with a Delphi dynamic array. A dynamic array is an array that is maintained by Delphi, and of which you can change the size using SetLength. It is declared like:
Код:
type
TIntegerArray = array of Integer;
Код:
type
TMonthArray = array of Month;
procedure AllKinds(const Arr: array of Month);
procedure OnlyDyn(Arr: TMonthArray);
Note: You should not forget that in Delphi, parameters can ony be declared with type specifications, and not with type declarations. So the following formal parameters, which would be type declarations, are not possible:
Код:
function Sum(const Items: array[1..7] of Integer): Integer;
procedure MoveTo(Spot: record X, Y: Integer; end);
Код:
type
TWeek = array[1..7] of Integer;
TSpot = record
X, Y: Integer;
end;
function Sum(const Items: TWeek): Integer;
procedure MoveTo(Spot: TSpot);
The latest Delphi compilers allow you to define dynamic array constants, so you can do:
Код:
var
MyDynArray: array of Integer;
begin
MyDynArray := [17, 325, 11];
Код:
ListAllIntegers([17, 325, 11]);
Код:
TArray<T>
Код:
type
...
TArray<T> = array of T;
Код:
procedure OnlyDyn(Arr: TArray<Month>);
Код:
constructor Create(Limbs: TArray<TLimb>; Negative: Boolean);
function ToByteArray: TArray<Byte>;
To use an open array from assembler, you must remember that an open array is in fact a combination of two parameters. The first parameter is a pointer to the start of the array, the second the adjusted High value. Generally, open arrays are passed as const or var. In all cases, the array is passed by reference.
Here is a simple example of a function that sums all integers in an array:
Код:
function Sum(const Data: array of Integer): Integer;
// EAX: address of the array
// EDX: adjusted High value
asm
MOV ECX,EAX // P := PInteger(Addr(Data));
XOR EAX,EAX // Result := 0;
OR EDX,EDX
JS @@Exit // if High(Data) < 0 then Exit;
@@Next: // repeat
ADD EAX,[ECX] // Result := Result + P^;
ADD ECX,TYPE Integer // Inc(PInteger(P));
DEC EDX // Dec(EDX);
JNS @@Next // until EDX < 0;
@@Exit:
end;
[/SHOWTOGROUPS]