This article describes the syntax and use of open array parameters, and the use of the "array of const" parameter type.
Open array parameters are special parameters which allow you to write procedures or functions that can act on any array of the same base type, regardless its size. To declare an open array parameter, you use a syntax like this:
procedure ListAllIntegers(AnArray: array of Integer); var I: Integer; begin for I := Low(AnArray) to High(AnArray) do WriteLn('Integer at ', I, ' is ', AnArray[I]); end;
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.
The code also demonstrates how you can determine the size of the array. 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.
you will see that the output is like this:
Integer at 0 is 17 Integer at 1 is 325 Integer at 2 is 11
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.
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 open an 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;
That will only display the first 6 values of the array, not all 12.
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(AnArray: Pointer; High: Integer);
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.
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]);
Here, the array is defined on the spot as [17, 325, 11]. The compiler ensures that the array exists during the call of the procedure, and it passes the correct High value. This is totally transparent to the code inside the procedure. After the call, the array is discarded.
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;
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.
type TMonthArray = array of Month; procedure AllKinds(Arr: array of Month); procedure OnlyDyn(Arr: TMonthArray);
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:
procedure Sum(const Items: array[1..7] of Integer); function MoveTo(Spot: record X, Y: Integer; end);
You'll have to declare a type first, and use the specifications as parameter type:
type TWeek = array[1..7] of Integer; TSpot = record X, Y: Integer; end; procedure Sum(const Items: TWeek); function MoveTo(Spot: TSpot);
That is why array of Something in a parameter list can't be a type declaration either. It is always an open array declaration.
Array of const is a special case of open arrays. Instead of passing only one type, you can pass a variety of types. Have a look at the declaration of the Format function in Delphi:
function Format(const Format: string; const Args: array of const): string;
(Actually, there is a second, overloaded function in some versions of Delphi, but I'll simply ignore that here.)
The first parameter is a string which indicates how you want your values formatted, and the second is an array of const, so you can pass a range of values using a similar syntax as the one for open array constructors. So you can call it like:
var Res: string; Int: Integer; Dub: Double; Str: string; begin Int := Random(1000); Dub := Random * 1000; Str := 'Teletubbies'; Res := Format('%4d %8.3f %s', [Int, Dub, Str]); end;
Internally, an array of const is an open array of TVarRec. The declaration of TVarRec is given in the online help for Delphi. It is a variant record, that contains a field called VType, and an overlay of different types, some of which are only pointers. The compiler creates a TVarRec for each member in the open array constructor, fills the VType field with the type of the member, and places the value, or a pointer to it, in one of the other fields. Then it passes the array of TVarRec to the function.
Since each TVarRec contains type information, Format can use this to check if it matches with the type given in the format string. That is why you get an error when passing the wrong type. You can tell the compiler that you want it to store a different type identifier, by casting to the desired type. If the type doesn't match exactly, the compiler will try to convert it to a matching type, so if you pass a Double, it will convert it to an Extended and pass that instead. Of course there are limitations on what the compiler can do.
Inside the function or procedure, you can treat the members as TVarRec immediately. The help for Delphi gives an example how to do this.
What you should notice is, that values in the TVarRec which are passed as pointers only exist during the course of the function or procedure. As soon as the routine ends, these values don't exist anymore. So you should not be tempted to return these pointers from the procedure or function, or to store the TVarRecs in an array outside the routine, unless you can make sure that you manage the values yourself.
If you must copy the TVarRecs to an array or variable outside the function (this can also be a var parameter), be sure to make a copy (i.e. on the heap) of the value, and replace the pointer in the TVarRec with one to the copy. You should also take care that the copy is disposed of when it is not needed anymore. An example follows:
type TConstArray = array of TVarRec; // Copies a TVarRec and its contents. If the content is referenced // the value will be copied to a new location and the reference // updated. function CopyVarRec(const Item: TVarRec): TVarRec; var W: WideString; begin // Copy entire TVarRec first. Result := Item; // Now handle special cases. case Item.VType of vtExtended: begin New(Result.VExtended); Result.VExtended^ := Item.VExtended^; end; vtString: begin New(Result.VString); Result.VString^ := Item.VString^; end; vtPChar: Result.VPChar := StrNew(Item.VPChar); // There is no StrNew for PWideChar. vtPWideChar: begin W := Item.VPWideChar; GetMem(Result.VPWideChar, (Length(W) + 1) * SizeOf(WideChar)); Move(PWideChar(W)^, Result.VPWideChar^, (Length(W) + 1) * SizeOf(WideChar)); end; // A little trickier: casting to string will ensure // reference counting is done properly. vtAnsiString: begin // Nil out first, so no attempt to decrement // reference count. Result.VAnsiString := nil; AnsiString(Result.VAnsiString) := AnsiString(Item.VAnsiString); end; vtCurrency: begin New(Result.VCurrency); Result.VCurrency^ := Item.VCurrency^; end; vtVariant: begin New(Result.VVariant); Result.VVariant^ := Item.VVariant^; end; // Casting ensures proper reference counting. vtInterface: begin Result.VInterface := nil; IInterface(Result.VInterface) := IInterface(Item.VInterface); end; // Casting ensures a proper copy is created. vtWideString: begin Result.VWideString := nil; WideString(Result.VWideString) := WideString(Item.VWideString); end; vtInt64: begin New(Result.VInt64); Result.VInt64^ := Item.VInt64^; end; vtUnicodeString: begin // Nil out first, so no attempt to decrement // reference count. Result.VUnicodeString := nil; UnicodeString(Result.VUnicodeString) := UnicodeString(Item.VAnsiString); end; // VPointer and VObject don't have proper copy semantics so it // is impossible to write generic code that copies the contents. end; end; // Creates a TConstArray out of the values given. Uses CopyVarRec // to make copies of the original elements. function CreateConstArray(const Elements: array of const): TConstArray; var I: Integer; begin SetLength(Result, Length(Elements)); for I := Low(Elements) to High(Elements) do Result[I] := CopyVarRec(Elements[I]); end; // TVarRecs created by CopyVarRec must be finalized with this function. // You should not use it on other TVarRecs. procedure FinalizeVarRec(var Item: TVarRec); begin case Item.VType of vtExtended: Dispose(Item.VExtended); vtString: Dispose(Item.VString); vtPChar: StrDispose(Item.VPChar); vtPWideChar: FreeMem(Item.VPWideChar); vtAnsiString: AnsiString(Item.VAnsiString) := ''; vtCurrency: Dispose(Item.VCurrency); vtVariant: Dispose(Item.VVariant); vtInterface: IInterface(Item.VInterface) := nil; vtWideString: WideString(Item.VWideString) := ''; vtInt64: Dispose(Item.VInt64); vtUnicodeString: UnicodeString(Item.VUnicodeString) := ''; end; Item.VInteger := 0; end; // A TConstArray contains TVarRecs that must be finalized. This function // does that for all items in the array. procedure FinalizeVarRecArray(var Arr: TConstArray); var I: Integer; begin for I := Low(Arr) to High(Arr) do FinalizeVarRec(Arr[I]); Finalize(Arr); Arr := nil; end;
The functions above can help you manage TVarRecs outside the routine for which they were constructed. You can even use a TConstArray where an open array is declared. The following little program
program VarRecTest; {$APPTYPE CONSOLE} uses SysUtils, VarRecUtils in 'VarRecUtils.pas'; var ConstArray: TConstArray; begin ConstArray := CreateConstArray([1, 'Hello', 7.9, IntToStr(1234)]); try WriteLn(Format('%d --- %s --- %0.2f --- %s', ConstArray)); Writeln(Format('%s --- %0.2f', Copy(ConstArray, 1, 2))); finally FinalizeConstArray(ConstArray); end; ReadLn; end.
will produce the expected, but not very exciting, output
1 --- Hello --- 7.90 --- 1234 Hello --- 7.90
The little program above also demonstrates how you can use Copy to use only a part of the entire TConstArray. Copy will create a copy of the dynamic array, but not copy the contents, so you should not try to use Copy and then later on use FinalizeConstArray on that copy. In the program above, the copy will be removed automatically, since the lifetime of the copy is managed by the compiler.
The unit VarRecUtils with the displayed functions can be downloaded from my Download page.
Open arrays and arrays of const are powerful features of the language, but they come with a few caveats. I hope I succeeded in showing some of these, and how you can overcome them.
Rudy Velthuis