Search code examples
delphidelphi-xe8

How to concat multiple strings with Move?


How can I concat an array of strings with Move. I tried this but I just cannot figure how to get Move operation working correctly.

program Project2;

{$POINTERMATH ON}

procedure Concat(var S: String; const A: Array of String);
var
  I, J: Integer;
  Len: Integer;
begin
  Len := 0;
  for I := 0 to High(A) do
  Len := Len + Length(A[I]);

  SetLength(S, Length(S) + Len);

  for I := 0 to High(A) do
  Move(PWideChar(A[I])[0], S[High(S)], Length(A[I]) * SizeOf(WideChar));
end;

var
  S: String;
begin
  S := 'test';
  Concat(S, ['test', 'test2', 'test3']);
end.

Solution

  • I'd write this function like so:

    procedure Concat(var Dest: string; const Source: array of string);
    var
      i: Integer;
      OriginalDestLen: Integer;
      SourceLen: Integer;
      TotalSourceLen: Integer;
      DestPtr: PChar;
    begin
      TotalSourceLen := 0;
      OriginalDestLen := Length(Dest);
      for i := low(Source) to high(Source) do begin
        inc(TotalSourceLen, Length(Source[i]));
      end;
      SetLength(Dest, OriginalDestLen + TotalSourceLen);
    
      DestPtr := PChar(Pointer(Dest)) + OriginalDestLen;
      for i := low(Source) to high(Source) do begin
        SourceLen := Length(Source[i]);
        Move(Pointer(Source[i])^, DestPtr^, SourceLen*SizeOf(Char));
        inc(DestPtr, SourceLen);
      end;
    end;
    

    It's fairly self-explanatory. The complications are caused by empty strings. Any attempt to index characters of an empty string will lead to exceptions when range checking is enabled.

    To handle that complication, you can add if tests for the case where one of the strings involved in the Move call is empty. I prefer a different approach. I'd rather cast the string variable to be a pointer. That bypasses range checking but also allows the if statement to be omitted.

    Move(Pointer(Source[i])^, DestPtr^, SourceLen*SizeOf(Char));
    

    One might wonder what happens if Source[i] is empty. In that case Pointer(Source[i]) is nil and you might expect an access violation. In fact, there is no error because the length of the move as specified by the third argument is zero, and the nil pointer is never actually de-referenced.

    The other line of note is here:

    DestPtr := PChar(Pointer(Dest)) + OriginalDestLen;
    

    We use PChar(Pointer(Dest)) rather than PChar(Dest). The latter invokes code to check whether or not Dest is empty, and if so yields a pointer to a single null-terminator. We want to avoid executing that code, and obtain the address held in Dest directly, even if it is nil.