Dynamic arrays — pure reference types, except when they’re not

A reasonable way to understand the semantics of dynamic arrays in Delphi is to recall the sort of code you might have used as a substitute before they where introduced in Delphi 4. Assuming only a single dimension to keep things simple, stage one would be to declare dummy static array type, together with a corresponding pointer type:

type
  PRectArray = ^TRectArray;
  TRectArray = array[0..$FFFFF] of TRect;

Allocation and reallocation may then be done using the appropriately-named ReallocMem routine. More exactly, you can use GetMem and FreeMem as well, though since ReallocMem can do both initial allocation and final deallocation, there’s no need — just remember to initialise the variable to nil if declared in a local routine, and always call ReallocMem at the end to free the array:

procedure ManualDynArrayExample(FirstSize, SecondSize: Integer);
var
  DynArray: PRectArray;

  procedure OutputInfo(Size: Integer); //we need to keep a record of the allocated size
  var
    I: Integer;
  begin
    Writeln('Output for when Size = ', Size, ':' + sLineBreak);
    for I := 0 to Size - 1 do
      Writeln('(', DynArray[I].Left, ', ', DynArray[I].Top, ') (',
    DynArray[I].Right, ', ', DynArray[I].Bottom, ')');
    Writeln('');
  end;
var
  I: Integer;
begin
  Writeln('*** ManualDynArrayExample ***', sLineBreak);
  DynArray := nil; //requires explicit initialisation in a local proc
  ReallocMem(DynArray, FirstSize * SizeOf(DynArray[0])); //Size param is in bytes
  try
    for I := 0 to FirstSize - 1 do
    DynArray[I] := Rect(I, I, I + 1, I + 1);
    OutputInfo(FirstSize);
    ReallocMem(DynArray, SecondSize * SizeOf(DynArray[0]));
    OutputInfo(SecondSize);
  finally //requires explicit resource protection
    ReallocMem(DynArray, 0);
  end;
end;

Now, this is certainly more complicated than using a ‘real’ dynamic array of TRect. Nonetheless, ReallocMem is the key routine here — all we’ve really done is a bit of housekeeping around it, and in so doing, made explicit what housekeeping the compiler does for us when we use dynamic arrays proper. Thus, the compiler-managed equivalent looks like this:

type
  TRectDynArray = array of TRect;

procedure CompilerDynArrayExample(FirstSize, SecondSize: Integer);
var
  DynArray: TRectDynArray;

  procedure OutputInfo;
  var
    I: Integer;
  begin
    Writeln('Output for when Size = ', Length(DynArray), ':' + sLineBreak);
    for I := 0 to Length(DynArray) - 1 do
      Writeln('(', DynArray[I].Left, ', ', DynArray[I].Top, ') (',
    DynArray[I].Right, ', ', DynArray[I].Bottom, ')');
    Writeln('');
  end;
var
  I: Integer;
begin
  Writeln('*** CompilerDynArrayExample ***', sLineBreak);
  SetLength(DynArray, FirstSize);
  for I := 0 to FirstSize - 1 do
    DynArray[I] := Rect(I, I, I + 1, I + 1);
  OutputInfo;
  SetLength(DynArray, SecondSize);
  OutputInfo;
end;

So, compiler-managed dynamic arrays are in essence their manual equivalent, only with boilerplate housekeeping being done for you. That in mind, consider the following test classes:

  TTestAnsiString = class
  strict private
    FData: AnsiString;
    function GetDataSize: Integer;
    procedure SetDataSize(Value: Integer);
  public
    property Data: AnsiString read FData;
    property DataSize: Integer read GetDataSize write SetDataSize;
  end;

  TAnsiCharDynArray = array of AnsiChar;

  TTestDynArray = class
  strict private
    FData: TAnsiCharDynArray;
    function GetDataSize: Integer;
    procedure SetDataSize(Value: Integer);
  public
    property Data: TAnsiCharDynArray read FData;
    property DataSize: Integer read GetDataSize write SetDataSize;
  end;

  TTestManual = class
  strict private
    FData: PAnsiChar;
    FDataSize: Integer;
    procedure SetDataSize(Value: Integer);
  public
    destructor Destroy; override;
    property Data: PAnsiChar read FData;
    property DataSize: Integer read FDataSize write SetDataSize;
  end;

(For their implemention, see the end of this post.) Now, what differences do TTestAnsiString and TTestDynArray exhibit from the point of view of code that calls them? One important one is Data being indexed from 1 in the first case but 0 in the second, though this isn’t really a reason against dynamic arrays as such. A second difference (one I mentioned in passing in my previous-but-one post) is though: for where TTestAnsiString properly encapsulates FData, TTestDynArray does not:

  TestDynArrayInst.Data[0] := 'T'; //compiles, despite being a read-only property
  TestAnsiStringInst.Data[1] := 'T'; //compiler error

This is a cost of dynamic arrays being essentially pure reference types, like classes — cf. how declaring an object parameter as ‘const’ does not stop the routine from calling a property setter or setting a public or published field of the passed-in object.

Well, given all this, the semantics of TTestDynArray.Data should be just like those of TTestManual.Data, right? Er, no… Consider this code:

procedure TestDynArray;
var
  Obj: TTestDynArray;
  SecondaryRef: TAnsiCharDynArray;

  procedure OutputInfo(const Title: string);
  begin
    Writeln(Title, ':');
    Writeln('Obj.DataSize'#9#9#9, Obj.DataSize);
    if Obj.DataSize <> 0 then
      Writeln('First element of Obj.Data'#9, Ord(Obj.Data[0]));
    Writeln('Length(SecondaryRef)'#9#9, Length(SecondaryRef));
    if Length(SecondaryRef) <> 0 then
      Writeln('First element of SecondaryRef'#9, Ord(SecondaryRef[0]));
    Writeln('SecondaryRef = Obj.Data?'#9, SecondaryRef = Obj.Data);
    Writeln('');
  end;
begin
  Writeln('*** TestDynArray ***', sLineBreak);
  Obj := TTestDynArray.Create;
  try
    SecondaryRef := Obj.Data;
    OutputInfo('After just created Obj and assigned SecondaryRef');

    Obj.DataSize := 200;
    OutputInfo('After set Obj.DataSize');

    SecondaryRef := Obj.Data; //must reassign as Obj.Data was nil originally!
    SecondaryRef[0] := #100;
    OutputInfo('After reassigned SecondaryRef and then set its first element to 100');

    SetLength(SecondaryRef, Length(SecondaryRef));
    OutputInfo('After called SetLength on SecondaryRef, passing in its current size');

    SecondaryRef[0] := #99;
    OutputInfo('After setting first element of SecondaryRef to 99');
  finally
    Obj.Free;
  end;
end;

When SecondaryRef[0] is assigned to #100, Obj.Data is implicitly set, since SecondaryRef at this point is what its name implies: a secondary reference. What happens with the SetLength call though? Despite being given the value of the existing length, it isn’t a no-op — instead, it has an effect akin to that of calling UniqueString on a string variable. When SecondaryRef[0] is set again, then, Obj.Data is left alone, since the link has been broken.

Cf. the direct equivalent for TTestManual:

procedure TestManual;
var
  Obj: TTestManual;
  SecondaryRef: PAnsiChar;

  procedure OutputInfo(const Title: PAnsiChar);
  begin
    Writeln(Title, ':');
    Writeln('Obj.DataSize'#9#9#9, Obj.DataSize);
    if Obj.DataSize <> 0 then
      Writeln('First element of Obj.Data'#9, Ord(Obj.Data[0]));
    if SecondaryRef <> nil then
      Writeln('First element of SecondaryRef'#9, Ord(SecondaryRef[0]));
    Writeln('SecondaryRef = Obj.Data?'#9, SecondaryRef = Obj.Data);
    Writeln('');
  end;
begin
  Writeln('*** TestManual ***', sLineBreak);
  Obj := TTestManual.Create;
  try
    SecondaryRef := Obj.Data;
    OutputInfo('After just created Obj and assigned SecondaryRef');

    Obj.DataSize := 200;
    OutputInfo('After set Obj.DataSize');

    SecondaryRef := Obj.Data;
    SecondaryRef[0] := #100;
    OutputInfo('After reassigned SecondaryRef and then set its first element to 100');

    ReallocMem(SecondaryRef, Obj.DataSize);
    OutputInfo('After called ReallocMem on SecondaryRef, passing in its current size');

    SecondaryRef[0] := #99;
    OutputInfo('After setting first element of SecondaryRef to 99');
  finally
    Obj.Free;
  end;
end;

One thing you may note here is that in contrast to the SetLength call, the ReallocMem one has the benefit of being patently hacky, since you’re making assumptions about how Data was allocated (obviously, it would be better not to have any ‘hacky’ code, but if it’s a must, it should at least announce itself in plain terms). More to the point though, it doesn’t break the link behind your back. Dynamic arrays, then, are pure reference types, except when they’re not!

I really don’t understand why SetLength behaves like it does here. If Delphi 4 were hot and fresh, I’d call it a bug — it’s as if the compiler engineers of the time had begun to duplicate the string type’s copy on write behaviour for dynamic arrays, but then changed their mind and didn’t roll back what they had done for it so far. Given we’re more than a decade down the line, I can’t see the behaviour ever being changed though, alas.

Appendix: full test code (replace ‘strict private’ with ‘private’ to compile in D7)

program ArrayTest;

{$APPTYPE CONSOLE}

uses
  Types,
  SysUtils;

type
  TTestAnsiString = class
  strict private
    FData: AnsiString;
    function GetDataSize: Integer;
    procedure SetDataSize(Value: Integer);
  public
    property Data: AnsiString read FData;
    property DataSize: Integer read GetDataSize write SetDataSize;
  end;

  TAnsiCharDynArray = array of AnsiChar;

  TTestDynArray = class
  strict private
    FData: TAnsiCharDynArray;
    function GetDataSize: Integer;
    procedure SetDataSize(Value: Integer);
  public
    property Data: TAnsiCharDynArray read FData;
    property DataSize: Integer read GetDataSize write SetDataSize;
  end;

  TTestManual = class
  strict private
    FData: PAnsiChar;
    FDataSize: Integer;
    procedure SetDataSize(Value: Integer);
  public
    destructor Destroy; override;
    property Data: PAnsiChar read FData;
    property DataSize: Integer read FDataSize write SetDataSize;
  end;

{ TTestAnsiString }

function TTestAnsiString.GetDataSize: Integer;
begin
  Result := Length(FData);
end;

procedure TTestAnsiString.SetDataSize(Value: Integer);
begin
  SetLength(FData, Value);
end;

{ TTestDynArray }

function TTestDynArray.GetDataSize: Integer;
begin
  Result := Length(FData);
end;

procedure TTestDynArray.SetDataSize(Value: Integer);
begin
  SetLength(FData, Value);
end;

{ TTestManual }

destructor TTestManual.Destroy;
begin
  ReallocMem(FData, 0);
  inherited;
end;

procedure TTestManual.SetDataSize(Value: Integer);
begin
  ReallocMem(FData, Value);
  FDataSize := Value;
end;

{ TestXXX procs }

procedure TestAnsiString;
var
  Obj: TTestAnsiString;
  SecondaryRef: AnsiString;

  procedure OutputInfo(const Title: string);
  begin
    Writeln(Title, ':');
    Writeln('Obj.DataSize'#9#9#9, Obj.DataSize);
    if Obj.DataSize <> 0 then
      Writeln('First element of Obj.Data'#9, Ord(Obj.Data[1]));
    Writeln('Length(SecondaryRef)'#9#9, Length(SecondaryRef));
    if Length(SecondaryRef) <> 0 then
      Writeln('First element of SecondaryRef'#9, Ord(SecondaryRef[1]));
    Writeln('SecondaryRef = Obj.Data?'#9, SecondaryRef = Obj.Data);
    Writeln('');
  end;
begin
  Writeln('*** TestAnsiString ***', sLineBreak);
  Obj := TTestAnsiString.Create;
  try
    SecondaryRef := Obj.Data;
    OutputInfo('After just created Obj and assigned SecondaryRef');

    Obj.DataSize := 200;
    OutputInfo('After set Obj.DataSize');

    SecondaryRef := Obj.Data;
    SecondaryRef[1] := #100;
    OutputInfo('After reassigned SecondaryRef and then set its first element to 100');

    SetLength(SecondaryRef, Length(SecondaryRef));
    OutputInfo('After called SetLength on SecondaryRef, passing in its current size');

    SecondaryRef[1] := #99;
    OutputInfo('After setting first element of SecondaryRef to 99');
  finally
    Obj.Free;
  end;
end;

procedure TestDynArray;
var
  Obj: TTestDynArray;
  SecondaryRef: TAnsiCharDynArray;

  procedure OutputInfo(const Title: string);
  begin
    Writeln(Title, ':');
    Writeln('Obj.DataSize'#9#9#9, Obj.DataSize);
    if Obj.DataSize <> 0 then
      Writeln('First element of Obj.Data'#9, Ord(Obj.Data[0]));
    Writeln('Length(SecondaryRef)'#9#9, Length(SecondaryRef));
    if Length(SecondaryRef) <> 0 then
      Writeln('First element of SecondaryRef'#9, Ord(SecondaryRef[0]));
    Writeln('SecondaryRef = Obj.Data?'#9, SecondaryRef = Obj.Data);
    Writeln('');
  end;
begin
  Writeln('*** TestDynArray ***', sLineBreak);
  Obj := TTestDynArray.Create;
  try
    SecondaryRef := Obj.Data;
    OutputInfo('After just created Obj and assigned SecondaryRef');

    Obj.DataSize := 200;
    OutputInfo('After set Obj.DataSize');

    SecondaryRef := Obj.Data; //must reassign as Obj.Data was nil originally!
    SecondaryRef[0] := #100;
    OutputInfo('After reassigned SecondaryRef and then set its first element to 100');

    SetLength(SecondaryRef, Length(SecondaryRef));
    OutputInfo('After called SetLength on SecondaryRef, passing in its current size');

    SecondaryRef[0] := #99;
    OutputInfo('After setting first element of SecondaryRef to 99');
  finally
    Obj.Free;
  end;
end;

procedure TestManual;
var
  Obj: TTestManual;
  SecondaryRef: PAnsiChar;

  procedure OutputInfo(const Title: PAnsiChar);
  begin
    Writeln(Title, ':');
    Writeln('Obj.DataSize'#9#9#9, Obj.DataSize);
    if Obj.DataSize <> 0 then
      Writeln('First element of Obj.Data'#9, Ord(Obj.Data[0]));
    if SecondaryRef <> nil then
      Writeln('First element of SecondaryRef'#9, Ord(SecondaryRef[0]));
    Writeln('SecondaryRef = Obj.Data?'#9, SecondaryRef = Obj.Data);
    Writeln('');
  end;
begin
  Writeln('*** TestManual ***', sLineBreak);
  Obj := TTestManual.Create;
  try
    SecondaryRef := Obj.Data;
    OutputInfo('After just created Obj and assigned SecondaryRef');

    Obj.DataSize := 200;
    OutputInfo('After set Obj.DataSize');

    SecondaryRef := Obj.Data;
    SecondaryRef[0] := #100;
    OutputInfo('After reassigned SecondaryRef and then set its first element to 100');

    ReallocMem(SecondaryRef, Obj.DataSize);
    OutputInfo('After called ReallocMem on SecondaryRef, passing in its current size');

    SecondaryRef[0] := #99;
    OutputInfo('After setting first element of SecondaryRef to 99');
  finally
    Obj.Free;
  end;
end;

{ other }

type
  PRectArray = ^TRectArray;
  TRectArray = array[0..$FFFFF] of TRect;

procedure ManualDynArrayExample(FirstSize, SecondSize: Integer);
var
  DynArray: PRectArray;

  procedure OutputInfo(Size: Integer); //we need to keep a record of the allocated size
  var
    I: Integer;
  begin
    Writeln('Output for when Size = ', Size, ':' + sLineBreak);
    for I := 0 to Size - 1 do
      Writeln('(', DynArray[I].Left, ', ', DynArray[I].Top, ') (',
        DynArray[I].Right, ', ', DynArray[I].Bottom, ')');
    Writeln('');
  end;
var
  I: Integer;
begin
  Writeln('*** ManualDynArrayExample ***', sLineBreak);
  DynArray := nil; //requires explicit initialisation in a local proc
  ReallocMem(DynArray, FirstSize * SizeOf(DynArray[0])); //Size param is in bytes
  try
    for I := 0 to FirstSize - 1 do
      DynArray[I] := Rect(I, I, I + 1, I + 1);
    OutputInfo(FirstSize);
    ReallocMem(DynArray, SecondSize * SizeOf(DynArray[0]));
    OutputInfo(SecondSize);
  finally //requires explicit resource protection
    ReallocMem(DynArray, 0);
  end;
end;

type
  TRectDynArray = array of TRect;

procedure CompilerDynArrayExample(FirstSize, SecondSize: Integer);
var
  DynArray: TRectDynArray;

  procedure OutputInfo;
  var
    I: Integer;
  begin
    Writeln('Output for when Size = ', Length(DynArray), ':' + sLineBreak);
    for I := 0 to Length(DynArray) - 1 do
      Writeln('(', DynArray[I].Left, ', ', DynArray[I].Top, ') (',
        DynArray[I].Right, ', ', DynArray[I].Bottom, ')');
    Writeln('');
  end;
var
  I: Integer;
begin
  Writeln('*** CompilerDynArrayExample ***', sLineBreak);
  SetLength(DynArray, FirstSize);
  for I := 0 to FirstSize - 1 do
    DynArray[I] := Rect(I, I, I + 1, I + 1);
  OutputInfo;
  SetLength(DynArray, SecondSize);
  OutputInfo;
end;

begin
  try
    ManualDynArrayExample(10, 20);
    CompilerDynArrayExample(10, 20);
    TestAnsiString;
    TestDynArray;
    TestManual;
    Readln;
  except
    on E:Exception do
      Writeln(E.Classname, ': ', E.Message);
  end;
end.
Advertisements

One thought on “Dynamic arrays — pure reference types, except when they’re not

  1. Interesting observations, though the curious behavior of SetLength(dynarray) i think makes then no less ref-types, than the existence of UniqueString makes strings less ref-types. But the behavior of SetLength is unexpected, truly 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s