I repeatedly see the usage of FreeAndNil for local variables in code, and often read in this forum the suggestion to use FreeAndNil because it is safer. I think that this is not just wrong, but makes it actually much harder to find bugs in the code. And I want to outline why, in an well engineered piece of code, you should not use FreeAndNil, unless in very specific situations that warrant it, and most defenetly, not use it just everywhere like it's a magic enchant that keeps memory related bugs away.
First a few basics, I will talk alout about bugs and errors, as well as exceptions and crashes. For many people, especially beginners, those that are the main recipients of the "always use FreeAndNil" advice, these terms may be somewhat interchangable. But here I am going to use them very specifically.
An
Error is when your program is in an unintended state. For example when you want to open a file, you expect the file to opend after the open function, but if, e.g. due to the file not existing, it can't be open, your program is now in the state that you expect the file to be open, but it's not because it can't be opend. You have an error. But errors can be more subtle than that, e.g. if you have a Vector type with X and Y coordinates, and that vector should always be normalized (i.e. sqrt(X*X + Y*Y) = 1), and you somehow manage to write X and Y coordinates that do not satisfy this property, you have an error, even though you may not recognize it when it occurs.
A
Bug is when you have code that is different from what actually intend it to be. Bugs come in all shapes, and can range from simple copy paste artifacts, where you forget to rename a variable name and use the copied name, other kind of bugs are when you use the same function, or you forget to check some conditions (e.g. divide by 0). A Bug does not necessarily cause an error, but can cause an error in certain situations.
For example:
var
sl1, sl2: TStringList;
begin
sl1 := TStringList.Create;
sl1.Free;
sl2 := TStringList.Create;
sl1.Free; // Bug: sl1 instead of sl2
end.
This is clearly a bug (and as a result of Copy and Pasting a bug that happens at least to me in every larger project at least once), but when you run it, it (using a "normal" PC with the standard memory manager) acctually does not produce an error, because it just happens that the memory manager allocates sl2 in the same memory as sl1 formally resided in, so sl1.Free is actually the exact same as sl2.Free.
So to summarize, a Bug is the piece of code that is wrong, but not necessarily produce an error when executed. Your goal as programmer is to reduce the number of bugs in your code. So you need a way to measure these bugs when they occur. This can be done with plain simple code reviews or static analysis. Another tool we as programmers have is testing the code, when the bug does not produce an error in the tested scenario, there is no way to find the bug, because everything is working as intended.
So as counter intuitive as it may seem at first, we actually want to turn our bugs into errors. Because errors can be measured, found and traced back to the bug. So our goal is to write tests that triggers the bugs to cause an error and measure that error.
There are different ways a program can response to an error. If the error is easiely detected, it can crash the program, or it can be cought and some code can be executed that tries to "fix" the errorneous state. E.g. if you try to open a file that does not exist, you may ask the user for a correct filename and try again.
In Pascal these two cases are often done with exceptions. When you open a File with Assign, and there is a problem, an exception will be raised to notify you of that error. You can either handle that exception, and try to get your program back into a working state, or if you ignore the exception your application will crash.
If you do not handle the error, a crash is actually the best case scenario, because then you at least know something went wrong. The alternative, if you don't notice the error, you may run in an errorneous state. For example, if you turn of exceptions for the File handling, when you can't open a file, simply nothing happens. There will be a global variable set, but if you don't check that (which is your bug), all of the following write operations will do nothing, but the user will not notice anything. This is by far the worst case situation, imagine a user working with your program for an hour or so, with the goal to produce a file, and then after hours of work, they see that the file was not created, and all their work was pointless.
So to recap, we want all of our bugs to trigger errors, and on error, we want to be able to detect them, and the best way to do so, is by actually causing a crash. When we talk about memory management, we have a few tools to turn errors into crashes. The two main ones are HeapTrc and Valgrind. In this thread I will mostly talk about HeapTrc, because it's the FPC native solution (and I am currently not on Linux to make Valgrind examples), but many of the points also hold for Valgrind.
So what I am now going to argue in the remainder of this post is why FreeAndNil is minimizing the effectiveness of those tools, with examples using HeapTrc, on how without FreeAndNil you would be finding bugs that FreeAndNil will coverup and therefore make harder to find.
Note: One of the main features of HeapTrc that will be used here is the heaptrc.keepreleased property, which enables you to find use-after-free bugs, by instead of actually freeing the memory on Free, it will keep it around, but taint it so it is unusable (at least for usage of Classes). This feature may not be feasable to be enabled on a whole program. But it should be the first thing to enable in UnitTests, which are specific test scenarios that only test single features of the application at a time, and because they are so small, there it is a must have.
Let's start first with the best case, when FreeAndNil is working as intended:
var
sl: TStringList
begin
sl := TStringList.Create;
FreeAndNil(sl);
sl.CaseSensitive := True; // Use after Free error, because it's called on nil it crashes
end.
This is the typical reasoning why you should use FreeAndNil, if you use the variable after nilling it, whenever the memory is accessed. Note that using "heaptrc.keepreleased := True;" will have a similar effect, as the object gets tainted, and valgrind would also flag every freed object and any use after free would be detected.
Note that unlike FreeAndNil, Heaptrc and Valgrind will also flag all references to that freed object:
var
sl1, sl2: TStringList
begin
sl1 := TStringList.Create;
sl2 := sl1;
FreeAndNil(sl1);
sl2.CaseSensitive := True; // sl2 is not nilled, even though it is the same freed object
end.
So even at it's best, FreeAndNil can help you spot some bugs, that you can also spot using the other tools at your disposal.
But where it gets really bad is when looking specifically at double free bugs, due to the interaction between Free and Nil (no pun intended). Free does not just Free an object, that is done by the destructor Destroy. Free checks if the object you call it on is nil, and if not it calls Destroy. Meaning if an object is set to Nil, Free (and therefore FreeAndNil) will simply do nothing.
Take this most simple example for a double Free:
var
sl: TStringList
begin
heaptrc.keepreleased := True;
sl := TStringList.Create;
sl.Free;
sl.Free; // Bug: Double Free
end.
In this setting it will always trigger a crashing error, but as we have seen in the example above, a double free may not always trigger a crash or even an error, if the memory that sl points to has been reallocated for another object in the meantime. This is why in this example we set "heaptrc.keepreleased := True;", so the memory is tainted and a double free will always trigger a crashing error. So this is good code, when we run it, it crashes, we spot the bug and correct it.
What happens if we use FreeAndNil? Well... nothing. Because FreeAndNil sets sl to Nil, so the second FreeAndNil will simply do nothing. This means, that here FreeAndNil will cause that Bug to not produce an error in the first place, which means we cannot detect this bug. While saving the programmer from an error being triggerd, also means that the programmer will likely not find that bug. So while it does not do harm to the program (in that it prevents an error), it also actively prevents the programmer from finding that bug and therefore improve their Code.
Note that the usage of FreeAndNil, by avoiding the call to the destructor, will also avoid the ability of HeapTrc or Valgrind to find this Bug, so if you use FreeAndNil you actually hinder the other tools to properly do their job.
It is similar with the very first example from this post:
var
sl1, sl2: TStringList;
begin
sl1 := TStringList.Create;
sl1.Free;
sl2 := TStringList.Create;
sl1.Free; // Bug: sl1 instead of sl2
end.
Without Heaptrc, Valgrind or FreeAndNil, as previously shown, this is currently a bug that does not produce an error. With FreeAndNil, the sl1.Free will simply not do anything, which causes a memory leak, which actually is an error, and detectable, but only at the end of the execution and not that easiely tracable. HeapTrc and Valgrind will give you a segfault crash on the second Free directly, making it much easier to spot and fix. Again FreeAndNil hinders the full potential of the other tools.
So to summarize, FreeAndNil can at most help you capture a subset of the Bugs that you can capture with other tools, while actively preventing the other tools from finding more bugs. It is therefore nearly always preferential to use the existing tools without FreeAndNil.
Lastly I want to say, there are good uses for FreeAndNil. For example, you write a Chat client, with your Form, when you loose connection, the connection object may be Destroyed. This means when the user clicks the "Send" button during that time, it should not work. In this case a good solution is, to on disconnect, FreeAndNil the connection object, and in the Send button, check if the connection is nil before using. The important part here is that this is a very specific design, where the Freeing can take place at different points (in time or in code), and you need to signal if the object was already freed or not. In the very most cases this is not the cases, but every Create has exactly one Free, and any more Frees are a bug that need to be detected.
So yes, in this very specific case FreeAndNil is a great solution, but that is the exception not the norm.
PS: I just want to note that the problem with FreeAndNil is simply the call to Free. Generally speaking this is actually more a Free than a FreeAndNil problem (even though it's the combination of those two that triggers the problems). If instead of Free simply an exception would be triggered when the object is nil, it would be much safer. Even more, just calling Destroy instead of Free would solve these problems, so a DestroyAndNil (what everyone can easiely write themselves) would solve all of the problems above, without giving up the convinience of nilling in case neither HeapTrc nor Valgrind is used (e.g. in production code, or during User Tests with the whole Program and not just the UnitTests).
So I could have actually titled this "PSA: Don't use Free", but Free is so widely used that getting rid of this habit is probably impossible, while for FreeAndNil, it's not that engrained in our brains