A Less Trivial Trie Library

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
2 Likes

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:

  1. Byte copy the source to an anonymous temporary object
  2. Call adjust on the temporary object
  3. Finalize the target
  4. Byte copy the temporary object to the target
  5. Adjust the target
  6. 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.

RM: Assignment and Finalization
AARM: Assignment and Finalization

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.

Yes, I’ve actually read that section several times before, and have already encountered a similar misunderstanding, but I usually skim the adjustment notes because I use Limited_Controlled types more than anything else.

I, at the very least, need to sit down and very carefully read this section again. Once again, I’m very grateful for the help.

1 Like

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!

1 Like

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.

1 Like