Are const parameters dangerous?
Marco Cantu's recent blog post The Case of Delphi Const String Parameters opened a can of worms.
A very, very old can...
The presented behavior is not something new, and it is certainly not a compiler bug.
It is just the way reference counting works. A pure ARC problem—one instance of
something is destroyed when the other one is created and assigned, because the
parameter did not trigger the reference counting mechanism—this is why
used for speed optimization in the first place.
The same thing that happens with strings happens with reference-counted objects, but it is easier to see what happens with objects in the same kind of code. You will need a form with a memo and a button to test the following code:
type IFoo = interface function GetCount: Integer; function GetNumber: Integer; end; TFoo = class(TInterfacedObject, IFoo) private FNumber: Integer; public constructor Create(ANumber: Integer); destructor Destroy; override; function GetCount: Integer; function GetNumber: Integer; end; constructor TFoo.Create(ANumber: Integer); begin inherited Create; FNumber := ANumber; Form1.Memo1.Lines.Add('Created ' + FNumber.ToString); end; destructor TFoo.Destroy; begin Form1.Memo1.Lines.Add('Destroyed ' + FNumber.ToString); inherited; end; function TFoo.GetCount: Integer; begin Result := FRefCount; end; function TFoo.GetNumber: Integer; begin Result := FNumber; end; procedure TForm1.Button1Click(Sender: TObject); var s1: IFoo; procedure Test(const Value: IFoo); begin s1 := TFoo.Create(456); Memo1.Lines.Add('Test Value ' + Value.GetNumber.ToString); end; begin s1 := TFoo.Create(123); Test(s1); end;
Output will be:
Created 123 Created 456 Destroyed 123 Test Value 123 Destroyed 456
It is pretty obvious that the object instance holding 123 is destroyed before its
value is printed.
Value.GetNumber.ToString is accessing a dangling pointer.
And with dangling pointers, anything can happen—if the memory previously occupied by the object or string has not been reused, you may get the expected value even though that instance is dead, but if the memory is reused for some other reference type in the meantime, you will read the contents of some other object. Also, the whole thing might crash, or it might not. Basically, you get undefined behavior.
A reference type consists of two parts: a variable that holds a pointer (address) to the actual memory location where the content (data) is located.
Managed reference type instances—ones that use ARC for managing their lifetime—are alive and point to valid memory as long as there is at least one strong reference to them. At the moment there are no more strong references, the memory allocated for that instance is released.
In theory, reference-counted variables should have only two states: either
(no associated data), or pointing to valid instance memory. However, under some
circumstances, it is possible to get weak references that can turn into dangling
pointers if the last strong reference to the instance is cleared in the meantime.
Such variables (references) represent non-zeroed weak references, and they will not be enough to keep the reference-counted instance alive, so they will still hold an address pointing to the now-invalid memory after the instance is destroyed.
This includes using raw pointer variables, using variables with the
attribute, and using function (procedure, method)
const parameters that don't trigger
the reference counting mechanism, where the reference itself is passed as a value
The following picture shows what happens with reference-counted object instances (the same applies to strings and dynamic arrays) in the above code:
The left side shows what happens if a value parameter is used in the
where reference counting is triggered and such parameter represents a strong
reference, while the right side shows what happens when a
const parameter is used in
After executing the line
s1 := TFoo.Create(456);
a new object instance holding 456 will be created and allocated in a different memory location from the one holding 123.
Value parameter is a strong reference (on the left), then it will still
point to the valid object instance in memory, and this instance will be destroyed
only after the
Value parameter goes out of scope at the end of the
However, if the
Value is a weak reference (on the right), then after assignment
of the new object to the original and only strong reference
s1, the object instance
holding 123 will be destroyed, and
Value will no longer point to the valid data.
There are several solutions to the above issue: declaring
Value as a value
parameter (not using
const), creating another strong reference using the local
Test procedure before the assignment to
s1 destroys the original, or
creating another strong reference to
s1 before the
Test procedure is called.
1. value parameter
procedure TForm1.Button1Click(Sender: TObject); var s1: IFoo; procedure Test(Value: IFoo); begin s1 := TFoo.Create(456); Memo1.Lines.Add('Test Value ' + Value.GetNumber.ToString); end; begin s1 := TFoo.Create(123); Test(s1); end;
2. strong reference in Test
procedure TForm1.Button1Click(Sender: TObject); var s1: IFoo; procedure Test(Value: IFoo); var s2: IFoo; begin s2 := Value; // or s2 := s1; s1 := TFoo.Create(456); Memo1.Lines.Add('Test Value ' + Value.GetNumber.ToString); end; begin s1 := TFoo.Create(123); Test(s1); end;
3. strong reference before calling Test
procedure TForm1.Button1Click(Sender: TObject); var s1, s2: IFoo; procedure Test(Value: IFoo); begin s1 := TFoo.Create(456); Memo1.Lines.Add('Test Value ' + Value.GetNumber.ToString); end; begin s1 := TFoo.Create(123); s2 := s1; Test(s1); end;
From the above examples, not using
const parameters seems like the cleanest and
most obvious solution. Which often leads to the wrong conclusion:
are dangerous and should not be used.
Are const parameters dangerous?
To answer this question, we have to go back to the beginning. Issues caused by accessing dangling pointers can be hard to track down. They are not commonly encountered when using object instances. However, the problem can more often be encountered when using strings, for several reasons.
First, strings in Delphi hold an almost magical aura, and they are often treated as
almost indestructible entities as long as they can be referenced. It is often
forgotten that they are just reference-counted instances, and that they can cease to exist
when there are no strong references to them. It is often forgotten that a string
field can be gone when its owning object instance is destroyed, and it is even
more often forgotten that using a
const parameter with a string will not keep that string
alive. Also, strings are commonly processed, parsed, modified and shuffled around,
and it is easy to overlook that some of the procedures used in that processing can
destroy the original data.
Because of all that, when developers encounter issues with strings for the first
time, they not only easily come to the conclusion that using
is dangerous and should be avoided in their code, they also want to fix that at
large, since the RTL uses
const string parameters almost everywhere. This results
in bug reports pointing to flaws in the compiler or the RTL that are rightfully
closed "As designed", which makes the whole situation even worse. You
have a frustrated developer that wasted some time chasing bugs that are then not
recognized as bugs.
The original purpose of Marco's blog post is to draw attention to the potential
issues and make developers more aware of them, while at the same time saying that
the compiler behavior around
const parameter will not change, nor will
parameters be generally removed from the core Delphi libraries.
Even if the compiler behavior around
const parameters will not change, that still
leaves room for requests to remove
const parameters and avoid their
usage in the RTL. Which brings us to the question "Are const parameters dangerous,
and should they be avoided in code?".
No, const parameters are not dangerous and they serve a purpose
const parameters are used with the purpose of avoiding unnecessary reference
counting triggers and the performance penalty they cause. If you think that such
optimization is unnecessary, you probably haven't been using the ARC compiler in
mobile development where non-const parameters on object references (triggering
reference counting under the ARC compiler) were causing major performance issues.
That is also one (not the only one) of the reasons the ARC compiler was abandoned in
const parameters are used for a reason, and the performance penalty for not
using them would simply be too great. Anyone asking for their removal from the core
Delphi libraries, or changing compiler behavior, is actually asking every Delphi
developer to pay a price they may not be able to afford.
What does that mean for library developers? Should they stop using
parameters? The answer is the same as for Delphi core libraries. Unless there is a
specific reason against it (read: a situation where almost every usage of particular
procedure, requires grabbing strong reference), it is better to use a
Situations where people using such a library will shoot themselves in the foot are extremely rare, and can be recognized and solved at the call site (like in example 3.) While there are certainly situations where spotting the problem might not be so easy, and that some dose of caution is always warranted, this is nothing extraordinary compared to the general caution required when writing any kind of code.
Note: There is known bug in the compiler when constructing object directly in function/procedure/method call. This is actual bug, and is not directly related to issues presented in this article.
Memory-Leak on Const-Parameter https://quality.embarcadero.com/browse/RSP-10100