Cast static array to open array of different element type

691 Views Asked by At

(I already asked this at CodeReview where it got closed as off-topic. Hopefully it's on-topic here.)

I have a static arrays of a derived type (like LabelsA: array[0..3] of TLabel; in the following sample code) and a routine accepting an open array of the base type (like procedure DoSomethingWithControls(const AControls: array of TControl);), and I want to call DoSomethingWithControls with those static arrays. Please see my sample:

procedure DoSomethingWithControls(const AControls: array of TControl);
var
  i: Integer;
begin
  for i := Low(AControls) to High(AControls) do
    Writeln(AControls[i].Name);
end;

procedure Test;
var
  LabelsA: array[0..3] of TLabel;
  LabelsB: array[0..1] of TLabel;

  procedure Variant1;
  type
    TArray1 = array[Low(LabelsA)..High(LabelsA)] of TControl;
    TArray2 = array[Low(LabelsB)..High(LabelsB)] of TControl;
  begin
    DoSomethingWithControls(TArray1(LabelsA));
    DoSomethingWithControls(TArray2(LabelsB));
  end;

  procedure Variant2;
  type
    TControlArray = array[0..Pred(MaxInt div SizeOf(TControl))] of TControl;
    PControlArray = ^TControlArray;
  begin
    DoSomethingWithControls(Slice(PControlArray(@LabelsA)^, Length(LabelsA)));
    DoSomethingWithControls(Slice(PControlArray(@LabelsB)^, Length(LabelsB)));
  end;

  procedure Variant3;
  var
    ControlsA: array[Low(LabelsA)..High(LabelsA)] of TControl absolute LabelsA;
    ControlsB: array[Low(LabelsB)..High(LabelsB)] of TControl absolute LabelsB;
  begin
    DoSomethingWithControls(ControlsA);
    DoSomethingWithControls(ControlsB);
  end;

begin
  Variant1;
  Variant2;
  Variant3;
end;

There are some possible variants of calling DoSomethingWithControls:

  • Variant 1 is quite simple but needs an "adapter" types like TArray1 for every size of TLabel array. I would like it to be more flexible.

  • Variant 2 is more flexible and uniform but ugly and error prone.

  • Variant 3 (courtesy of TOndrej) is similar to Variant 1 - it doesn't need an explicit cast, but Variant 1 offers a tiny bit more compiler security if you mess something up (e.g. getting the array bounds wrong while copy-pasting).

Any ideas how i can formulate these calls without these disadvantages (without changing the element types of the arrays)? It should work with D2007 and XE6.

5

There are 5 best solutions below

6
On BEST ANSWER

These casts are all rather ugly. They will all work, but using them makes you feel dirty. It's perfectly reasonable to use a helper function:

type
  TControlArray = array of TControl;

function ControlArrayFromLabelArray(const Items: array of TLabel): TControlArray;
var 
  i: Integer;
begin
  SetLength(Result, Length(Items));
  for i := 0 to high(Items) do
    Result[i] := Items[i];
end;

And then you call your function like this:

DoSomethingWithControls(ControlArrayFromLabelArray(...));

Of course, this would be so much cleaner if you could use generics.

8
On

Not extremely beautiful either but you could trick the compiler like this:

procedure Variant3;
var
  ControlsA: array[Low(LabelsA)..High(LabelsA)] of TControl absolute LabelsA;
begin
  DoSomethingWithControls(ControlsA);
end;
13
On

Since a dynamic array can be passed into method as an open array, and option would be to convert the static array to a dynamic array.

If you don't mind the overhead of copying the array, consider the following:

Write a function to convert an open array of labels into a dynamic TControlArray array.

type
  TControlArray = array of TControl;

{$IFOPT R+} {$DEFINE R_ON} {$R-} {$ENDIF}
function MakeControlArray(const ALabels: array of TLabel): TControlArray;
begin
  SetLength(Result, Length(ALabels));
  Move(ALabels[0], Result[0], Length(ALabels) * SizeOf(TObject));
end;
{$IFDEF R_ON} {$R+} {$UNDEF R_ON} {$ENDIF}

Now Variant4 can be written as:

procedure Variant4;
begin
  DoSomethingWithControls(MakeControlArray(LabelsA));
  DoSomethingWithControls(MakeControlArray(LabelsB));
end;

Test cases:

procedure TAdHocTests.TestLabelsToControls;
const
  LLabelsA: array[0..3] of TLabel = (Pointer(0),Pointer(1),Pointer(2),Pointer(3));
var
  LLoopI: Integer;
  LLabelsB: array[0..9] of TLabel;
  LEmptyArray: TLabelArray;
begin
  for LLoopI := Low(LLabelsB) to High(LLabelsB) do
  begin
    LLabelsB[LLoopI] := Pointer(LLoopI);
  end;

  DoSomethingWithControls(MakeControlArray(LLabelsA), Length(LLabelsA));
  DoSomethingWithControls(MakeControlArray(LLabelsB), Length(LLabelsB));
  DoSomethingWithControls(MakeControlArray([]), 0);
  DoSomethingWithControls(MakeControlArray(LEmptyArray), 0);
end;

procedure TAdHocTests.DoSomethingWithControls(
    const AControls: array of TControl;
    AExpectedLength: Integer);
var
  LLoopI: Integer;
begin
  CheckEquals(AExpectedLength, Length(AControls), 'Length incorrect');
  for LLoopI := Low(AControls) to High(AControls) do
  begin
    CheckEquals(LLoopI, Integer(AControls[LLoopI]));
  end;
end;
0
On

Declare an overloaded procedure:

procedure DoSomethingWithControls(const AControls: array of TControl); overload;
var
  i: Integer;
begin
  for i := 0 to High(AControls) do
    if Assigned(AControls[i]) then
       Writeln(AControls[i].Name)
    else
      WriteLn('Control item: ',i);
end;

procedure DoSomethingWithControls(const ALabels: array of TLabel); overload;
type
  TControlArray = array[0..Pred(MaxInt div SizeOf(TControl))] of TControl;
  PControlArray = ^TControlArray;
begin
  DoSomethingWithControls(Slice(PControlArray(@ALabels)^, Length(ALabels)));
end;

This is a general solution to your variant2. One declaration for all cases, so less prone to errors.

3
On

Below example is based on how open array parameters are internally implemented. It won't work with "typed @ operator" however.

  procedure Variant4;
  type
    TCallProc = procedure (AControls: Pointer; HighBound: Integer);
  var
    CallProc: TCallProc;
  begin
    CallProc := @DoSomethingWithControls;

    CallProc(@LabelsA, Length(LabelsA) - 1);
    CallProc(@LabelsB, Length(LabelsB) - 1);
  end;

Passing High(Labels) for HighBound is perhaps better as long as all static arrays are 0 based.