9

(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.

8
  • why not use TObjectList<T>.ToArray? Commented Jul 22, 2015 at 8:06
  • @whosrdaddy: Sorry, forgot to mention D2007. :-/ Commented Jul 22, 2015 at 8:08
  • I'm surprised that you can't just pass an array of TLabel Commented Jul 22, 2015 at 8:25
  • @David: Yeah, I guess that's that co(ntra?)-variance thingy. :-) Commented Jul 22, 2015 at 8:28
  • 1
    @J... The compiler passes the length info when you pass a static array to an open array param Commented Jul 22, 2015 at 10:12

5 Answers 5

3

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.

Sign up to request clarification or add additional context in comments.

6 Comments

My OCD reflex is to complain about the (neglibible, I know) memory allocation and copy overhead of the dynamic array which ideally shouldn't be needed. It would be really nice if one could return an open array like Slice does (with a little help from the compiler). :-)
You could also make an overload for DoSomethingWithControls that takes an array of TLabel to make it neater.
I agree regarding heap allocation but this is GUI code and won't be your bottleneck surely.
@UliGerhardt I would keep that OCD in check because premature optimization is bad.
@NewWorld, I try to. But wouldn't it be nice if there was a beautiful, elegant, zero-overhead solution? ;-)
|
1

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;

8 Comments

Thanks, @TOndrej! Unfortunately this suffers from the same drawback as Variant1 - I'd have to declare an absolute variable for every call.
I agree it's a drawback although strictly speaking, it's not the same as Variant1. You only said you wanted to avoid declaring a new adapter type. ;-)
OK, Variant3 has an additional drawback: It compiles even if I get the array size wrong. Variant1 wouldn't compile. ;-)
If I mistakenly write TArray2 = array[Low(LabelsA)..High(LabelsA)] of TControl; in Variant1 (note the A instead of the correct B) I get "E2089 Invalid typecast". ControlsB: array[Low(LabelsA)..High(LabelsA)] of TControl absolute LabelsB; (same error!) happily compiles.
IMO, absolute should be removed from the language.
|
1

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.

Comments

1

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.

2 Comments

Hmm, it works but it's hardly elegant. Btw, why not use high(..) rather than Length(..) - 1.
@David - In case a non-zero based static array is passed. Otherwise I agree high is better as I stated in the last sentence.
0

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;

13 Comments

@DavidHeffernan True, passing [] or an empty dynamic array can cause a range error. But this is easily resolved by disabling range-checking specifically for that function. Also, in the specified use-case, why (and for that matter how) would you declare a zero length static array?
Your function doesn't operate on static arrays. It accepts an open array and returns a dynamic array. Your edit now enables range checking for all code below this function. Not what is needed.
@DavidHeffernan I could point out that if my function doesn't operate on static arrays, then neither does yours. (Note: you should check your function a little more closely.... you'll find it won't even compile.) ..... However, perhaps you should rather look at the DUnit test case I added and elaborate how my function "doesn't operate on static arrays"?.
You misunderstand me. Your function accepts more than static arrays. It can certainly be passed zero length arrays.
The type checking is a little academic in this case. We all know that TLabel derives from TControl. But it's usually preferable to have the compiler verify these things. I don't see any harm in using a loop. Performance won't be an issue. The loop will work under ARC unlike Move. What's the benefit of using Move?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.