How to concat multiple strings with Move?
Asked Answered
G

2

5

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.
Gladsome answered 3/11, 2015 at 1:43 Comment(1)
The RTL has such a function. See _LStrCatN and its Unicode counterpart in System.pas. The compiler automatically generates calls to that function when you concatenate more than three strings in a single statement, as in s1 + s2 + s3 + s4 + s5. The RTL function doesn't have quite the same interface; it accepts its list of strings on the stack instead of as an array. You cannot call the RTL function directly unless you write assembly.Deniable
C
7

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.

Corrigible answered 3/11, 2015 at 9:13 Comment(2)
There's no need to assert anything. The entire point of writing it this way is that it works with empty strings. What could you possibly assert anyway? The code should admit nil pointers, they are valid input.Corrigible
Also first answer has call @UniqueStringU and call @UStrToPWChar in the loop which would further degrade performance!Gladsome
H
3

In the second loop you forget that S already has the right size to get filled with all the elements so you have to use another variable to know the destination parameter of Move

procedure Concat(var S: String; const A: Array of String);
var
  I, Len, Sum: Integer;
begin
  Len := 0;
  for I := 0 to High(A) do
    Inc(Len, Length(A[I]));
  Sum := Length(S);
  SetLength(S, Sum + Len);
  for I := 0 to High(A) do
  begin
    if Length(A[I]) > 0 then
      Move(A[I][1], S[Sum+1], Length(A[I]) * SizeOf(Char));
    Inc(Sum, Length(A[I]));
  end;
end;  

Casting the source parameter to PWideChar is totally superfluous since the Move function use a kind of old generic syntax that allows to pass everything you want (const Parameter without type).

Hydro answered 3/11, 2015 at 1:58 Comment(5)
Accessing the first character of an AnsiString using [] notation will raise an exception if the string is empty. Check either A[I] <> nil or Length(A[I]) > 0 before indexing the string, or else use a PAnsiChar typecast instead (which is safe when the string is empty): PAnsiChar(A[I])^. Also, initializing a local variable at the same time as declaring it (var Sum: Integer = 1;) is not legal in Delphi. you are also re-allocating S to include the original data of S, but not starting Sum at the end of the original data, so you are going to overwrite it instead of append to it.Thurman
@user15124: Unless asserts are disabled, that is.Thurman
@Gladsome if you use the code form the answer you got then it would work just fine.Ney
This code had another error which I have fixed. In the call to Move you must use S[Sum+1] rather than the original S[Sum] because string indexing is one-based.Corrigible
@DavidHeffernan, string indexing could be zero indexed though. But your answer is better since it is indifferent to ZeroBasedStrings settings.Autolycus

© 2022 - 2024 — McMap. All rights reserved.