Adjust doesn’t need to deallocate it. When you do assignment of controlled types, Finalize is called on the original object prior to Adjust. The Finalize call will deallocate the object for you. That is why Unbounded_Strings don’t leak memory on assignment. The same thing when copying indefinite_holders. They don’t leak on copy.
EDIT: quick thrown together example:
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Finalization; use Ada.Finalization;
with Ada.Unchecked_Deallocation;
procedure jdoodle is
package Holders is
type Holder is tagged private;
function Get(Self : Holder) return String;
procedure Set(Self : in out Holder; Value : String);
private
type String_Access is access String;
type Holder is new Controlled with record
Element : String_Access := null;
end record;
overriding procedure Adjust(Self : in out Holder);
overriding procedure Finalize(Self : in out Holder);
end Holders;
package body Holders is
function Get(Self : Holder) return String is
(if Self.Element = null then "" else Self.Element.all);
procedure Set(Self : in out Holder; Value : String) is begin
Self.Finalize;
Put("Holders.Set => ");
Self.Element := new String'(Value);
Put_Line(Self.Element.all);
end Set;
procedure Adjust(Self : in out Holder) is
Temp : String_Access := Self.Element;
begin
if Temp /= null then
Put("Holders.Adjust => ");
Self.Element := new String'(Temp.all);
Put_Line(Self.Element.all);
end if;
end Adjust;
procedure Finalize(Self : in out Holder) is
procedure Free is new Ada.Unchecked_Deallocation(String, String_Access);
begin
if Self.Element /= null then
Put_Line("Holders.Finalize => " & Self.Element.all);
Free(Self.Element);
end if;
end Finalize;
end Holders;
use Holders;
Thing_1, Thing_2 : Holder;
begin
Put_Line("Setting initial values");
Thing_1.Set("Hello World");
Thing_2.Set("Bonjour");
Put_Line("Copying a value");
Thing_2 := Thing_1;
Put_Line("Finished!");
end jdoodle;
Results:
Setting initial values
Holders.Set => Hello World
Holders.Set => Bonjour
Copying a value
Holders.Finalize => Bonjour
Holders.Adjust => Hello World
Finished!
Holders.Finalize => Hello World
Holders.Finalize => Hello World
Oh hell, I was ignorant of that part of adjustment. Well, that settles that, use of Unbounded_String should be recommended with my trie library, and this solves the entire problem.
I greatly appreciate the patience in explaining this to me. I really need to sit down and read the whole AARM someday.
If you need it, the pertinent section is 7.6 (see links below). One interesting note, is that the default language behavior actually has the following process:
Byte copy the source to an anonymous temporary object
Call adjust on the temporary object
Finalize the target
Byte copy the temporary object to the target
Adjust the target
The temporary object is finalized
But it allows for the results up in my example above if the compiler can verify the anonymous temporary object isn’t necessary.
paragraphs 13-15 detail what an “assignment operation” is and paragraph 17 discusses the default operation order which includes the assignment operation.
A lot of stuff below details how implementations can avoid the anonymous temporary object.
EDIT:
If the anonymous object is present the results of my example might look more like:
Setting initial values
Holders.Set => Hello World
Holders.Set => Bonjour
Copying a value
Holders.Adjust => Hello World //Temporary object adjusted
Holders.Finalize => Bonjour
Holders.Adjust => Hello World
Holders.Finalize => Hello World // Temporary object finalized
Finished!
Holders.Finalize => Hello World
Holders.Finalize => Hello World
The final practical result is the same though, you just get one extra allocation (adjusted temporary object) and deallocation (Finalized temporary object).
That said, I have rarely seen GNAT do the temporary object for my code.
One note on your UDP. I noticed that your Finalize operation calls Close on the socket. What happens if you call Close on the socket again afterwards (Some close operations raise an exception when closing an already closed object, some just gracefully skip)?
I ask because Finalize is required to be idempotent because the implementation is allowed to call Finalize twice on an object in some situations. See Section 7.6.1 paragraph 24:
If it does raise an exception, you may want to wrap the Close call in an IF checking if it is open already first. If it gracefully skips, then you don’t need to do anything.
The Finalize calls Close, which is the UNIX close, but always ignores its result. The underlying POSIX_UDP_Garbage does nothing with the return value of close. I suppose it would be possible for the file descriptor to be reused between Finalize calls, but the type is neither visibly controlled nor nonlimited, so even this shouldn’t be an issue. I was aware of many issues and minutia of Controlled types, including that only Initialize may raise an exception, at the least.
Ok good. I only asked cause I ran into this yesterday when I had a Finalize procedure close a File handle and Text_IO raises a Status_Error exception if you call Close twice on a file handle, so once I figured out why my code was tossing exceptions at the end, I wrapped my Close call in an IF on Is_Open.
I knew the idempotency rule, but in my head, I somehow thought Text_IO.Close would just ignore it if already closed. I should have read up!
It seems Controlled types are a particularly tricky part of the language. That’s all the more reason to carefully read the section and familiarize oneself with all of the rules. I don’t recall if I already knew the idempotency rule, but I’m certainly going to reread this section again soon enough, and pay very careful attention this time, since it’s befuddled me twice by now.