Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing slice of a static/dynamic array by reference with start and length specifier

Passing arrays (dynamic or static) to methods/procedures/functions with open array parameters, declaration can look like this:

procedure WorkWithArray( const anArray : array of Integer);
(* or procedure WorkWithArray( var anArray : array of Integer); *)
var
  i : Integer;
begin
  for i := Low(anArray) to High(anArray) do
  begin
    // Do something with the "open array" anArray
    WriteLn(anArray[i]);
  end;
end;

...
var
  staticArray : array[0..2] of Integer;
  dynArray : array of integer;
  dynArrayG : TArray<Integer>;
begin
  SetLength(dynArray,10);
  SetLength(dynArrayG,10);

  WorkWithArray(staticArray);  // Using a static array
  WorkWithArray(dynArray);     // Using a dynamic array
  WorkWithArray(dynArrayG);    // Using a dynamic generic array
  ...
end;

Passing arrays like this is a very common idiom used throughout the Delphi RTL, including some very optimized functions/procedures for handling arrays of data.


Suppose we need to call WorkWithArray with a subrange of our arrays. We can then use the intrinsic Slice() function.

First without an offset, starting with first index:

Type
  // Helper declarations
  TIntLongArray = array[0..MaxInt div SizeOf(Integer) - 1] of integer;
  PIntLongArray = ^TIntLongArray;

WorkWithArray(Slice(staticArray,2)); // No type cast needed for static arrays
WorkWithArray(Slice(PIntLongArray(@dynArray)^,2));
WorkWithArray(Slice(PIntLongArray(@dynArrayG)^,2));

Note: dynamic arrays does not fit directly into the Slice() function, see "Slice does not work with dynamic arrays". So the workaround method with type casting has to be used.


What if we want to work with a subrange not starting from the first element?

Doable as well:

WorkWithArray(Slice(PIntLongArray(@staticArray[1])^,2));
WorkWithArray(Slice(PIntLongArray(@dynArray[1])^,2));
WorkWithArray(Slice(PIntLongArray(@dynArrayG[1])^,2));

Note : the sum of the offset and the slice must not exceed the element count of the array.

I know that using Copy(myArray,x1,x2) could be used in cases where the input is declared as a const, but this will make a copy of the the array, and is ineffiecient for large arrays. (With risk of stack overflow as well).


Finally, my question:

While this demonstrates a way to pass a subrange of an array by reference with a start index and a length specifier, it looks a bit awkward. Are there better alternatives and if so how?

like image 305
LU RD Avatar asked Apr 13 '13 22:04

LU RD


1 Answers

Updated See a bit down for a generics solution.

Here is an alternative that encapsulates the type cast needed for the offset inside a function, which resides in an advanced record declared as a class function. Besides hiding the type cast, the offset is range checked against the high index of the array.

More types can be added if needed.

Type
  SubRange = record
    Type
      TIntLongArray = array[0..MaxInt div SizeOf(Integer) - 1] of integer;
      PIntLongArray = ^TIntLongArray;
      TByteLongArray = array[0..MaxInt div SizeOf(Byte) - 1] of Byte;
      PByteLongArray = ^TByteLongArray;

    class function Offset( const anArray : array of Integer; 
                                 offset  : Integer) : PIntLongArray; overload; static;
    class function Offset( const anArray : array of Byte; 
                                 offset  : Integer) : PByteLongArray; overload; static;
    // ToDo: Add more types ...
  end;

class function SubRange.Offset(const anArray : array of Integer; 
                                     offset  : Integer): PIntLongArray;
begin
  Assert(offset <= High(anArray));
  Result := PIntLongArray(@anArray[offset]);
end;

class function SubRange.Offset(const anArray : array of Byte; 
                                     offset  : Integer): PByteLongArray;
begin
  Assert(offset <= High(anArray));
  Result := PByteLongArray(@anArray[offset]);
end;

Note : the sum of the offset and the slice must not exceed the element count of the array.

Example calls:

WorkWithArray( Slice(SubRange.Offset(staticArray,1)^,2));
WorkWithArray( Slice(SubRange.Offset(dynArray,1)^,2));
WorkWithArray( Slice(SubRange.Offset(dynArrayG,1)^,2));

While this looks better, I'm still not convinced this is the optimal solution.


Update

When writing the above solution, I had a generics solution as the ultimate goal.

Here is an answer that utilizes anonymous methods and generics to implement a Slice(anArray,startIndex,Count) method that can be used with both static and dynamic arrays.

A straight generics solution would rely on range checking be turned off at every placed where it was used, and that would not be a pretty solution. The reason is that SizeOf(T) could not be used to declare a static array type of maximum size:

TGenericArray = array[0..MaxInt div SizeOf(T) - 1] of T; // SizeOf(T) not resolved

So we would have to use:

TGenericArray = array[0..0] of T;

instead. And this triggers the range check when it is on, for index > 0.

Solution

But the problem could be solved by another strategy, callbacks or a more modern terminology would be Inversion of Control (IoC) or Dependeny Injection (DI). The concept is best explained with, "Don't call me, we call you".

Instead of using a direct function, we pass the operational code as an anonymous method together with all parameters. Now the range check problem is contained within the Slice<T> frame.

Slice<Integer>.Execute(
  procedure(const arr: array of Integer)
  begin
    WriteLn(Math.SumInt(arr));
  end, dArr, 2, 7);

unit uGenericSlice;

interface

type
  Slice<T> = record
  private
    type
      PGenericArr = ^TGenericArr;
      TGenericArr = array [0..0] of T;
  public
    type
      TConstArrProc = reference to procedure(const anArr: array of T);
    class procedure Execute(       aProc: TConstArrProc;
                             const anArray: array of T;
                                   startIndex,Count: Integer); static;
  end;

implementation

class procedure Slice<T>.Execute(aProc: TConstArrProc;
  const anArray: array of T; startIndex, Count: Integer);
begin
  if (startIndex <= 0) then
    aProc(Slice(anArray, Count))
  else
  begin
    // The expression PGenericArr(@anArray[startIndex]) can trigger range check error
    {$IFOPT R+}
      {$DEFINE RestoreRangeCheck}
      {$R-}
    {$ENDIF}
    Assert((startIndex <= High(anArray)) and (Count <= High(anArray)-startIndex+1),
      'Range check error');
    aProc(Slice(PGenericArr(@anArray[startIndex])^, Count));
    {$IFDEF RestoreRangeCheck}
      {$UNDEF RestoreRangeCheck}
      {$R+}
    {$ENDIF}
  end;
end;

end.

Here are some example use cases:

program ProjectGenericSlice;

{$APPTYPE CONSOLE}

uses
  Math,
  uGenericSlice in 'uGenericSlice.pas';

function Sum(const anArr: array of Integer): Integer;
var
  i: Integer;
begin
  Result := 0;
  for i in anArr do
    Result := Result + i;
end;

procedure SumTest(const arr: array of integer);
begin
  WriteLn(Sum(arr));
end;

procedure TestAll;
var
  aProc: Slice<Integer>.TConstArrProc;
  dArr: TArray<Integer>;
  mySum: Integer;
const
  sArr: array [1 .. 10] of Integer = (
    1,2,3,4,5,6,7,8,9,10);

begin
  dArr := TArray<Integer>.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

  aProc :=
    procedure(const arr: array of Integer)
    begin
      WriteLn(Sum(arr));
    end;

  // Test predefined anonymous method
  Slice<Integer>.Execute( aProc, dArr, 2, 7);

  // Test inlined anonymous method
  Slice<Integer>.Execute(
    procedure(const arr: array of Integer)
    begin
      WriteLn(Sum(arr));
    end, dArr, 2, 7);

  // Test call to Math.SumInt
  Slice<Integer>.Execute(
    procedure(const arr: array of Integer)
    begin
      WriteLn(Math.SumInt(arr));
    end, dArr, 2, 7);

  // Test static array with Low(sArr) > 0
  Slice<Integer>.Execute(
    procedure(const arr: array of Integer)
    begin
      WriteLn(Sum(arr));
    end, sArr, 3 - Low(sArr), 7);

  // Using a real procedure
  Slice<Integer>.Execute(
    SumTest, // Cannot be nested inside TestAll
    dArr, 2, 7);

  // Test call where result is passed to local var
  Slice<Integer>.Execute(
    procedure(const arr: array of Integer)
    begin
      mySum := Math.SumInt(arr);
    end, dArr, 2, 7);
  WriteLn(mySum);

end;

begin
  TestAll;
  ReadLn;
end.
like image 149
LU RD Avatar answered Oct 08 '22 12:10

LU RD