The Case of Delphi Const String Parameters

Since the introduction of the long, reference counted, string type in early versions of Delphi, the compiler has offered the option to skip the increase and decrease operations on the reference count for a string parameter, by declaring it as a const parameter. For functions or methods called a large number of times in a loop, there is a noticeable difference in function call overhead using const string parameters.

For this reason in recent years, there has been a strong push in the Delphi community for Delphi developers and in particular library writers to pass all string parameters as const, unless the functions and methods modify these strings. However, there are potential issues using this technique, which I think are important to understand by Delphi developers.

The Core Problem

The issue stems from the fact that for a constant string parameter the compiler takes the string reference and doesn't "manage" it (adds no reference counting or does any other operation), treating it like a pointer to the memory location. The compiler, as expected, properly checks that the code of the function that receives the string parameter doesn't change it. 

However, the compiler has no control on what happens to the original string the parameter refers to. Changes to the original string can affect its memory layout and location. The compiler will protect you from such an issue in the case of a regular string parameter (strings with multiple references do a copy-on-write operation automatically). But in case of a const parameter, the compiler does not increase the reference count of the original string and therefore the original string can be modified like if it had no other reference. In other words, a change to the original string makes the const string parameter referring to that string invalid, and the use of this string parameter would most likely cause a memory access error.

Let's See This in Code

To clarify, let's look at this code:

procedure TForm1.Button1Click(Sender: TObject);
var
s1: string;

procedure Test(const Value: string);
begin
   s1 := '456';
   ShowMessage(Value);
end;

begin
s1 := Copy('123', 1);
Test(s1);
end;

​The s1 string has an initial value of 123 (computed, as constant strings behave differently in terms of reference counting). This is passed to the Value parameter. But the code changes s1 (the string aliased by Value). What happens to Value? This is undefined -- the first time I execute it, the message shows "OK" and the second time it shows some random text and crashes. But the effect will change.

For the specific code there are several workarounds, like copying the Value to a temp variable.

procedure Test(const Value: string);
begin
   var s2 := Value;
   s1 := '456';
   ShowMessage(s2);
end;

Now before you dismiss this example as being hardly found in the real world, we recently faced a bug in the database portion of our RTL library (in particular related with TField.GetValue) that had this exact root cause. In a specific case, a customer ended up writing apparently correct code that ended up trashing the underlying value of a const string parameter causing a memory corruption for the string. Making a local copy of the parameter solves the issue.

So What Should Delphi Developers Do?

This is the Delphi compiler behavior, and it has been like this for a fairly long time. I'm not expecting this to change, because if a developer wants the compiler to manage the string and its lifetime properly, the solution is simply to pass it as a regular string parameter, incurring the very limited overhead required to manage the reference count and gain a safety net.

If a developer is looking for maximum performance, knowing the string won't change, and passes the string as const, the compiler should not interfere, avoid adding any extra hidden calls, and accept the developer recommendation.

Any significant change in the compiler after many years can affect existing code in terms of performance and other assumptions the developer made on the behavior of passing string parameters. 

If the compiler isn't going to change, what's the effect on libraries? Simple operations on strings, not affecting the parameter itself, should keep using a const string parameter. So for example, core string functions should remain as they are and be converted to use const string parameters if they are not. 

However using const string parameters in more complex scenarios and large libraries, causes some potential risk. That's because in theory any method accepting a const string argument and calling other subroutines (which can directly or indirectly affect the original string) may be affected by the problem. Any developer designing Delphi libraries should keep considering the use of const string parameters as an optimization, but at the same time assess the risk this practice can expose the library to.

 

Leave Your Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.