Memory Management in Ada — When to Use Access Types, Controlled Types, and Storage Pools
· Go Komura · Ada, GNAT, Memory Management, Access Types, RAII, Storage Pools
Revision history (first version, published Nov 1, 2026)
- First published
Cite this article(DOI: 10.5281/zenodo.23856526)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Memory Management in Ada — When to Use Access Types, Controlled Types, and Storage Pools. KomuraSoft LLC. https://doi.org/10.5281/zenodo.23856526 https://comcomponent.com/en/blog/ada-memory-management/
- DOI (latest version)
- 10.5281/zenodo.23856526
- DOI (this version)
- 10.5281/zenodo.23856527
The Ada standard does not assume garbage collection (GC). GNAT’s default allocator uses malloc and free and does not automatically reclaim memory that is no longer used1. Instead, Ada has ways of writing code that avoid the heap altogether, and mechanisms for writing the rules that apply when you do use the heap into types.
This article explains memory management in Ada in the following order:
- Put values on the stack. Even values whose length is decided at run time can be returned from functions without writing
new. - When the heap is needed, use access types, and decide with types what they can point to and how long it lives.
- Deallocate with Unchecked_Deallocation. As the name says, it is unchecked, so reduce the places that call it directly.
- Leave deallocation to the Finalize of a controlled type. This is the same idea as RAII in C++.
- Decide where memory comes from with storage pools. You can set limits and count allocations.
All examples were compiled and run with GNAT 13.3.0 on Ubuntu 24.04, and the output is shown as is. valgrind 3.22.0 was used to check for memory misuse. The language rules were checked against the Ada 2022 Reference Manual (RM). The characteristics of Ada itself are introduced in “The Appeal of the Ada Language”.
Where values are placed in Ada
In Ada, declared variables are normally placed on the stack. Even arrays whose length is decided at run time can be declared as variables without writing new. The heap (a storage pool, in Ada terminology) is used when you write new2.
flowchart TB
accTitle: Where values are placed in Ada
accDescr: Shows that declared variables go on the stack, return values of unknown length go on the secondary stack in GNAT, and values created with new go in the access type's storage pool.
D["Declared variables"] --> S["Stack"]
F["Return values of unknown length"] --> SS["Secondary stack (GNAT)"]
N["Values created with new"] --> P["Storage pool"]
P --> M["The default pool uses malloc and free"]
Figure 1: Declared variables go on the stack, and values created with new go in a storage pool.
Grouped by where the memory lives, the mechanisms covered in this article are as follows.
| Location | Allocation | Deallocation | Section of this article |
|---|---|---|---|
| Stack | Variable declaration | When leaving the scope | Returning values of unknown length without new |
| Secondary stack (GNAT) | A function that returns a value of unknown length | When leaving the caller’s scope | Same as above |
| Storage pool | new |
Unchecked_Deallocation, or controlled types and containers |
Access types onward |
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (15 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
Returning values of unknown length without new
In C, to return a string whose length is decided at run time, you normally either have the caller provide a buffer or malloc inside the function and have the caller free it. In Ada, a String of unknown length can be the return value as is.
with Ada.Text_IO; use Ada.Text_IO;
procedure Repeat_Demo is
-- The length of the return value is not known until the call
function Repeat (C : Character; N : Natural) return String is
Result : constant String (1 .. N) := (others => C);
begin
return Result;
end Repeat;
N : Natural := 3;
begin
for I in 1 .. 3 loop
declare
Line : constant String := Repeat ('*', N);
begin
Put_Line (Line & " length =" & Natural'Image (Line'Length));
end;
N := N * 2;
end loop;
end Repeat_Demo;
*** length = 3
****** length = 6
************ length = 12
There is no new and no deallocation. The length of Line is decided by the value Repeat returns. Running this program under valgrind showed zero heap allocations.
The GNAT in this article’s environment places such return values on the secondary stack. The GNAT binder documentation describes the secondary stack as the place for “objects of unconstrained types (for example, an unconstrained String) returned by functions”3. In many environments the secondary stack grows as needed and is implemented as a chain of blocks on the heap3. The GNAT documentation also says that only some targets use the secondary stack for function return values4.
I checked whether the memory is reused after leaving the block, with a loop that returns a 1 MB string 10,000 times.
with Ada.Text_IO; use Ada.Text_IO;
procedure SS_Loop is
function Repeat (C : Character; N : Natural) return String is
Result : constant String (1 .. N) := (others => C);
begin
return Result;
end Repeat;
Total : Long_Long_Integer := 0;
begin
-- Return a 1 MB string 10,000 times, about 10 GB in total
for I in 1 .. 10_000 loop
declare
Line : constant String := Repeat ('*', 1_000_000);
begin
Total := Total + Long_Long_Integer (Line'Length);
end;
end loop;
Put_Line ("total =" & Long_Long_Integer'Image (Total));
end SS_Loop;
total = 10000000000
==32596== in use at exit: 1,000,048 bytes in 1 blocks
==32596== total heap usage: 1 allocs, 0 frees, 1,000,048 bytes allocated
Even after returning 10 GB of strings in total, there was only one heap allocation, of 1,000,048 bytes. That one allocation is presumably the block for growing the secondary stack; the iterations after it made no new allocations. Unlike values created with new, the program does not need to write any deallocation.
flowchart TB
accTitle: How the secondary stack is used
accDescr: Shows that when a function returns a value of unknown length, it is pushed onto the secondary stack, used in the caller's block, and released when leaving the block.
C["Call Repeat"] --> R["Push the result onto the secondary stack"]
R --> U["Use it as Line in the declare block"]
U --> E["Leave the block"]
E --> B["Release the pushed memory"]
Figure 2: A return value of unknown length is released when leaving the caller’s block.
You can confirm this GNAT usage by adding pragma Restrictions (No_Secondary_Stack). This restriction guarantees at compile time that the generated code does not reference the secondary stack4. Compiling the same function as Repeat above under this restriction gave the following errors.
pragma Restrictions (No_Secondary_Stack);
procedure Nosec is
function Repeat (C : Character; N : Natural) return String is
Result : constant String (1 .. N) := (others => C);
begin
return Result;
end Repeat;
Line : constant String := Repeat ('*', 3);
begin
null;
end Nosec;
nosec.adb:7:07: error: violation of restriction "No_Secondary_Stack" at line 1
nosec.adb:10:04: error: violation of restriction "No_Secondary_Stack" at line 1
nosec.adb:10:30: error: violation of restriction "No_Secondary_Stack" at line 1
In code under this restriction, the caller prepares an array of fixed length and the function writes into it.
Deciding “what it can point to” with access types
You need the heap when you want to use a value outside the scope that created it, or when elements are linked to each other, as in a list or a tree. In Ada, you use access types for this. An access type is close to a C pointer, but its declaration decides what it can point to2.
Pool-specific and general access types
A type declared with just access is a pool-specific access type. It can only point to values created with that type’s new (elements of its storage pool). A type declared with access all is a general access type, and it can also point to variables declared with aliased2.
with Ada.Text_IO; use Ada.Text_IO;
procedure Access_Kinds is
-- Pool-specific access type: can only point to what new allocated
type Int_Ptr is access Integer;
-- General access type: can also point to aliased variables
type Int_Ref is access all Integer;
P : constant Int_Ptr := new Integer'(10);
Counter : aliased Integer := 20;
R : constant Int_Ref := Counter'Access;
begin
R.all := R.all + 1;
Put_Line ("P.all =" & Integer'Image (P.all));
Put_Line ("Counter =" & Integer'Image (Counter));
end Access_Kinds;
P.all = 10
Counter = 21
The opposite combinations are compile errors: trying to point to a variable with a pool-specific type, and trying to point to a variable without aliased.
procedure Access_Errors is
type Int_Ptr is access Integer;
type Int_Ref is access all Integer;
Plain : Integer := 1;
Counter : aliased Integer := 2;
P1 : Int_Ptr := Counter'Access; -- A pool-specific type cannot point to a variable
R1 : Int_Ref := Plain'Access; -- Cannot point to a variable that is not aliased
begin
null;
end Access_Errors;
access_errors.adb:8:27: error: result must be general access type
access_errors.adb:8:27: error: add "all" to type "Int_Ptr" defined at line 2
access_errors.adb:9:20: error: prefix of "Access" attribute must be aliased
Thanks to this rule, you can tell from the declarations which variables may be pointed to.
flowchart TB
accTitle: Kinds of access types and what they can point to
accDescr: Shows that a pool-specific access type points only to values created with new, and a general access type can point to values created with new as well as variables declared aliased.
PS["Pool-specific access type (access)"] --> NEW["Values created with new"]
GA["General access type (access all)"] --> NEW
GA --> AL["Variables declared aliased"]
PL["Variables without aliased"] -.-> X["No access type can point to them"]
Figure 3: Only general access types can point to variables declared aliased.
Lifetime checks (accessibility checks)
A common mistake in C is to store the address of a function’s local variable somewhere that survives after the function returns. Ada prevents this with accessibility checks. If a variable’s lifetime (accessibility level) is shorter than the access type’s lifetime, a value pointing to that variable cannot be stored in that type5.
When it can be determined at compile time, it is a compile error. The following package tries to store a value pointing to a parameter variable in Saved, a library-level variable (one that lives until the end of the program).
package Registry is
type Int_Ref is access all Integer;
Saved : Int_Ref; -- A library-level variable; it lives until the end of the program
procedure Remember_Static (X : aliased in out Integer);
end Registry;
package body Registry is
procedure Remember_Static (X : aliased in out Integer) is
begin
Saved := X'Access;
end Remember_Static;
end Registry;
registry.adb:4:16: error: non-local pointer cannot point to local object
If the parameter is of an anonymous access type (access Integer), what the caller passes is not known at compile time. In that case the check is done at run time, and if it fails, Program_Error is raised5.
package Registry is
type Int_Ref is access all Integer;
Saved : Int_Ref; -- A library-level variable; it lives until the end of the program
Global_Value : aliased Integer := 100;
procedure Remember (X : access Integer);
end Registry;
package body Registry is
procedure Remember (X : access Integer) is
begin
Saved := Int_Ref (X); -- The actual parameter's lifetime is checked at run time
end Remember;
end Registry;
with Ada.Text_IO; use Ada.Text_IO;
with Registry; use Registry;
procedure Remember_Demo is
procedure Try_Local is
Local : aliased Integer := 1;
begin
Remember (Local'Access);
Put_Line ("saved a local object");
end Try_Local;
begin
Remember (Global_Value'Access);
Put_Line ("saved:" & Integer'Image (Saved.all));
Try_Local;
end Remember_Demo;
saved: 100
raised PROGRAM_ERROR : registry.adb:4 accessibility check failed
Passing the library-level Global_Value could be stored, while passing the local variable of Try_Local stopped at the point of storing it. If you do the same thing in C, the store succeeds, and using that value after the function returns is undefined behavior. In Ada, it stops on the line that tries to store it.
flowchart TB
accTitle: When accessibility checks are performed
accDescr: Shows that trying to store a value pointing to a short-lived variable in a long-lived access type is a compile error if it can be determined at compile time, and raises Program_Error if it cannot be known until run time, as with a parameter of an anonymous access type.
A["Store a value pointing to a short-lived variable"] --> Q{"Is the lifetime known at compile time?"}
Q -->|"Yes"| CE["Compile error"]
Q -->|"No (anonymous access type parameter)"| RT["Checked at run time"]
RT --> PE["Program_Error"]
Figure 4: Lifetime checks are done at compile time when possible, and at run time otherwise.
Access types that exclude null
A variable of an access type is null unless you give it an initial value. Adding not null to the type of a parameter or variable makes that type exclude null2. If null is passed, Constraint_Error is raised at the point of the call.
with Ada.Text_IO; use Ada.Text_IO;
procedure Not_Null_Demo is
type Int_Ptr is access Integer;
-- The type promises that the parameter is never null
procedure Show (P : not null Int_Ptr) is
begin
Put_Line ("value =" & Integer'Image (P.all));
end Show;
A : constant Int_Ptr := new Integer'(5);
B : Int_Ptr; -- An access variable with no initial value is null
begin
Show (A);
Show (B);
end Not_Null_Demo;
not_null_demo.adb:13:04: warning: variable "B" is read but never assigned [-gnatwv]
value = 5
raised CONSTRAINT_ERROR : not_null_demo.adb:16 access check failed
The first is the compile-time warning, the second the run-time output. Show no longer needs to check for null, and when null is passed, the error is reported at the calling line (line 16) rather than inside Show.
Deallocating with Unchecked_Deallocation
A value allocated with new is deallocated with a procedure instantiated from Ada.Unchecked_Deallocation. By convention this procedure is named Free. Under the RM rules, X becomes null after Free (X), and if X is already null, nothing happens6.
As the word Unchecked in the name says, this procedure does not look at other pointers to the same memory. In the following example, A is deallocated while A and B point to the same value.
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Unchecked_Deallocation;
procedure Dangling_Demo is
type Int_Ptr is access Integer;
procedure Free is new Ada.Unchecked_Deallocation (Integer, Int_Ptr);
A : Int_Ptr := new Integer'(42);
B : constant Int_Ptr := A; -- A second pointer to the same object
begin
Free (A);
Put_Line ("A = null : " & Boolean'Image (A = null));
Put_Line ("B = null : " & Boolean'Image (B = null));
Put_Line ("B.all =" & Integer'Image (B.all)); -- Read the freed memory
end Dangling_Demo;
A = null : TRUE
B = null : FALSE
B.all = 1613378576
A became null, but B still holds the original address. The RM calls such a value, which points to an object that no longer exists, a dangling reference, and says that an execution that dereferences it is erroneous (the result is not defined by the standard)6. With GNAT 13.3, no exception was raised, and B.all returned a value other than 42. Running the same program under valgrind reports that freed memory was read.
==6788== Invalid read of size 4
==6788== at 0x10BAD8: _ada_dangling_demo (dangling_demo.adb:14)
==6788== by 0x10B8DE: main (b~dangling_demo.adb:248)
==6788== Address 0x5004040 is 0 bytes inside a block of size 4 free'd
==6788== at 0x484988F: free (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==6788== by 0x4B08E48: __gnat_free (in /usr/lib/x86_64-linux-gnu/libgnat-13.so)
==6788== by 0x10B954: _ada_dangling_demo (dangling_demo.adb:11)
The trace also shows that GNAT’s default pool calls C’s free through __gnat_free.
flowchart TB
accTitle: The pointer left behind after Unchecked_Deallocation
accDescr: Shows that Free(A) makes A null, but B, which pointed to the same memory, keeps the original address, dereferencing B is erroneous, and valgrind reports it as a read of freed memory.
S["A and B point to the same value"] --> F["Free (A)"]
F --> AN["A becomes null"]
F --> BD["B keeps the original address"]
BD --> R["Read B.all"]
R --> E["Erroneous (result undefined)"]
E -.-> V["valgrind: Invalid read"]
Figure 5: Free sets only the variable you pass to null; other variables pointing to the same memory remain.
Conversely, if you do not deallocate, the memory is not returned. The following loop assigns the result of new to the same variable 1,000 times, losing the pointer to the previous value each time.
procedure Leak_Demo is
type Int_Ptr is access Integer;
P : Int_Ptr;
begin
for I in 1 .. 1_000 loop
P := new Integer'(I); -- The pointer to the previous memory is lost here
end loop;
end Leak_Demo;
==10504== in use at exit: 4,000 bytes in 1,000 blocks
==10504== 4,000 bytes in 1,000 blocks are definitely lost in loss record 1 of 1
==10504== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==10504== by 0x4B08DE1: __gnat_malloc (in /usr/lib/x86_64-linux-gnu/libgnat-13.so)
==10504== by 0x10A6FF: _ada_leak_demo (leak_demo.adb:6)
Memory that a language with GC would reclaim stayed allocated until the end of the program with GNAT. The GNAT documentation also says that the default pool (System.Pool_Global) uses malloc and free and does not reclaim memory automatically1.
The standard does not assume GC either. In the Ada 2012 revision, the text about garbage collection was removed from the Reference Manual. The Annotated Reference Manual (AARM) says that instead Ada 2012 provides subpools, which are much more predictable than GC7.
SPARK’s ownership rules
SPARK, a subset of Ada, imposes ownership rules on access types. Only one access value at a time can read and write the allocated memory8. I wrote the same sequence as the earlier Dangling_Demo as SPARK code and checked it with GNATprove 15.1 using --mode=check_all.
with Ada.Unchecked_Deallocation;
procedure Dangling_Spark with SPARK_Mode is
type Int_Ptr is access Integer;
procedure Free is new Ada.Unchecked_Deallocation (Integer, Int_Ptr);
A : Int_Ptr := new Integer'(42);
B : Int_Ptr := A; -- Ownership of A moves to B
V : Integer;
begin
Free (A);
V := B.all;
Free (B);
end Dangling_Spark;
dangling_spark.adb:11:10: error: "A" is not readable
11 | Free (A);
| ^ here
object was moved at line 8 [E0010]
8 | B : Int_Ptr := A; -- Ownership of A moves to B
| ^ here
Ownership moves to B at B := A, so the check stopped the line that tries to deallocate A. In Dangling_Demo, compiled and run with GNAT, the same mistake raised no exception and was only found with valgrind. How to proceed with proofs in SPARK is covered in “Proving AI-Written Code with SPARK”.
flowchart TB
accTitle: How Ada and SPARK differ in handling the same mistake
accDescr: Shows that the mistake of pointing two pointers at the same memory and freeing one of them goes undetected and reads freed memory when run as Ada, while checked as SPARK, GNATprove reports an error because ownership was moved.
P["Point two pointers at the same memory and free it"] --> ADA["Run it as Ada"]
P --> SP["Check it as SPARK"]
ADA --> R["Reads freed memory without detection"]
SP --> E["GNATprove reports an error due to the ownership move"]
Figure 6: In SPARK, the same mistake is found at the checking stage by the ownership rules.
If you write in plain Ada, limit the places that call Free to a few spots where it is clear who owns the value. The controlled types below are the main example.
Cleaning up when leaving a scope with controlled types
If you derive from the Controlled type in Ada.Finalization, you can override three procedures9.
| Procedure | When it is called |
|---|---|
Initialize |
When the object is declared without an initial value |
Adjust |
After a copy by assignment, on the target of the copy |
Finalize |
When leaving the scope, before being overwritten by an assignment, and when deallocated with Free |
To check the order in which they are called, I made a type that prints its name each time one is called. In Adjust, the name is lowercased so you can tell which object is the copy.
with Ada.Finalization;
package Tracked is
type Resource is new Ada.Finalization.Controlled with record
Name : Character := '?';
end record;
overriding procedure Initialize (R : in out Resource);
overriding procedure Adjust (R : in out Resource);
overriding procedure Finalize (R : in out Resource);
end Tracked;
with Ada.Characters.Handling; use Ada.Characters.Handling;
with Ada.Text_IO; use Ada.Text_IO;
package body Tracked is
overriding procedure Initialize (R : in out Resource) is
begin
Put_Line (" Initialize " & R.Name);
end Initialize;
-- Lowercase the name so you can tell it is the copy
overriding procedure Adjust (R : in out Resource) is
begin
Put_Line (" Adjust " & R.Name & " -> " & To_Lower (R.Name));
R.Name := To_Lower (R.Name);
end Adjust;
overriding procedure Finalize (R : in out Resource) is
begin
Put_Line (" Finalize " & R.Name);
end Finalize;
end Tracked;
with Ada.Finalization;
with Ada.Text_IO; use Ada.Text_IO;
with Tracked; use Tracked;
procedure Raii_Demo is
begin
Put_Line ("enter block");
declare
-- No initial value, so Initialize is called
A : Resource;
-- Initialized with an aggregate, so Initialize is not called
B : Resource := (Ada.Finalization.Controlled with Name => 'B');
begin
A.Name := 'A';
Put_Line ("assign B := A");
B := A;
Put_Line ("leave block");
end;
Put_Line ("after block");
Put_Line ("enter block with exception");
begin
declare
C : Resource;
begin
C.Name := 'C';
raise Constraint_Error;
end;
exception
when Constraint_Error =>
Put_Line ("handled Constraint_Error");
end;
end Raii_Demo;
enter block
Initialize ?
assign B := A
Finalize B
Adjust A -> a
leave block
Finalize a
Finalize A
after block
enter block with exception
Initialize ?
Finalize C
handled Constraint_Error
The output shows the following:
Initializewas called only forA, which had no initial value, and not forB, which was initialized with an aggregate. The RM also states thatInitializeis called on default initialization9.- For
B := A, firstFinalizewas called onB, which is being overwritten, and thenAdjuston the copied value. - When leaving the block,
Finalizewas called in the reverse order of declaration (the copy inB, thenA). Finalizewas also called forC, which left its block through an exception.
flowchart TB
accTitle: The order in which controlled type procedures are called
accDescr: Shows that Initialize is called for a declaration without an initial value, that an assignment calls Finalize on the overwritten side and Adjust on the copy, and that leaving the scope or leaving by an exception calls Finalize in the reverse order of declaration.
D["Declare without an initial value"] --> I["Initialize"]
I --> AS["Execute B := A"]
AS --> F1["Finalize B's old value"]
F1 --> AD["Adjust the copied value"]
AD --> L["Leave the scope or exit by an exception"]
L --> F2["Finalize in reverse order of declaration"]
Figure 7: An assignment calls Finalize on the old value and Adjust on the copy, and leaving the scope calls Finalize in reverse order.
Because Finalize is also called when leaving by an exception, writing the heap deallocation here means the caller can no longer forget to write Free. This is the same idea as RAII in C++, where deallocation is written in the destructor. RAII on the C++ side is covered in “Practical Multithreading Best Practices: C++ Edition”.
For resources you do not want copied, use Limited_Controlled. It makes a type that cannot be assigned, so there is no Adjust. The example in the next section uses it.
Deciding where memory comes from with storage pools
Where new takes memory from is decided by the storage pool of each access type. If you specify nothing, the implementation’s default pool is used; in GNAT, that is System.Pool_Global, which uses malloc and free1. In Ada you can replace this pool per type or put a limit on it10.
Setting a limit with Storage_Size
When you specify Storage_Size for an access type, new for that type allocates from an implementation-defined pool with at least the specified amount of memory. When that memory is used up, it allocates no more and raises Storage_Error10.
with Ada.Text_IO; use Ada.Text_IO;
procedure Bounded_Demo is
-- Limit the memory available to new for this type to 64 bytes
type Int_Ptr is access Integer
with Storage_Size => 64;
P : Int_Ptr;
Count : Natural := 0;
begin
loop
P := new Integer'(Count);
Count := Count + 1;
end loop;
exception
when Storage_Error =>
Put_Line ("Storage_Error after" & Natural'Image (Count) & " allocations");
end Bounded_Demo;
Storage_Error after 8 allocations
After 8 allocations, the 9th new raised Storage_Error. How much one allocation uses is decided by the implementation, so the count cannot simply be derived from the size of Integer. The memory of this pool is reclaimed when leaving the scope where the access type was declared10. You can use this when you want to cap memory use per type, for example in embedded devices where you want a fixed memory limit, or in processes that run for a long time.
flowchart TB
accTitle: Allocation with an access type that specifies Storage_Size
accDescr: Shows that executing new with an access type that specifies Storage_Size allocates if the pool has free space, raises Storage_Error if it is used up, and that the pool's memory is reclaimed when leaving the scope where the access type was declared.
N["Execute new with Int_Ptr"] --> Q{"Does the pool have free space?"}
Q -->|"Yes"| A["Allocate"]
Q -->|"No"| E["Storage_Error"]
A --> S["Leave the scope where the access type was declared"]
S --> R["Reclaim the pool's memory"]
Figure 8: A type with Storage_Size raises Storage_Error when its budget is used up, and the whole budget is reclaimed when leaving the scope.
Writing your own pool
You can make your own pool by deriving from Root_Storage_Pool in System.Storage_Pools and overriding Allocate, Deallocate, and Storage_Size10. The following pool counts allocations, deallocations, and bytes in use, and leaves the actual allocation to GNAT’s default pool. System.Pool_Global is a GNAT-specific unit, so other implementations need a different place to allocate from.
with System;
with System.Storage_Elements; use System.Storage_Elements;
with System.Storage_Pools; use System.Storage_Pools;
package Counting_Pools is
-- A pool that counts allocations, deallocations, and bytes in use
type Counting_Pool is new Root_Storage_Pool with record
Allocations : Natural := 0;
Deallocations : Natural := 0;
In_Use : Storage_Count := 0;
end record;
overriding procedure Allocate
(Pool : in out Counting_Pool;
Storage_Address : out System.Address;
Size_In_Storage_Elements : Storage_Count;
Alignment : Storage_Count);
overriding procedure Deallocate
(Pool : in out Counting_Pool;
Storage_Address : System.Address;
Size_In_Storage_Elements : Storage_Count;
Alignment : Storage_Count);
overriding function Storage_Size
(Pool : Counting_Pool) return Storage_Count;
procedure Report (Pool : Counting_Pool; Label : String);
end Counting_Pools;
with Ada.Text_IO; use Ada.Text_IO;
with System.Pool_Global; -- GNAT's default pool (a GNAT-specific unit)
package body Counting_Pools is
Backing : System.Pool_Global.Unbounded_No_Reclaim_Pool
renames System.Pool_Global.Global_Pool_Object;
overriding procedure Allocate
(Pool : in out Counting_Pool;
Storage_Address : out System.Address;
Size_In_Storage_Elements : Storage_Count;
Alignment : Storage_Count) is
begin
Backing.Allocate (Storage_Address, Size_In_Storage_Elements, Alignment);
Pool.Allocations := Pool.Allocations + 1;
Pool.In_Use := Pool.In_Use + Size_In_Storage_Elements;
end Allocate;
overriding procedure Deallocate
(Pool : in out Counting_Pool;
Storage_Address : System.Address;
Size_In_Storage_Elements : Storage_Count;
Alignment : Storage_Count) is
begin
Backing.Deallocate (Storage_Address, Size_In_Storage_Elements, Alignment);
Pool.Deallocations := Pool.Deallocations + 1;
Pool.In_Use := Pool.In_Use - Size_In_Storage_Elements;
end Deallocate;
overriding function Storage_Size
(Pool : Counting_Pool) return Storage_Count is
begin
return Storage_Count'Last; -- No upper limit
end Storage_Size;
procedure Report (Pool : Counting_Pool; Label : String) is
begin
Put_Line (Label & ": allocations =" & Natural'Image (Pool.Allocations)
& ", deallocations =" & Natural'Image (Pool.Deallocations)
& ", in use =" & Storage_Count'Image (Pool.In_Use) & " bytes");
end Report;
end Counting_Pools;
This pool is combined with the Limited_Controlled type from the previous section. Buffer is a type that holds exactly one array on the heap and frees it in Finalize. The access type Byte_Array_Ptr specifies Storage_Pool => Pool, so that new for this type goes through Counting_Pool.
with Ada.Finalization;
with Counting_Pools;
package Buffers is
Pool : Counting_Pools.Counting_Pool;
type Byte_Array is array (Positive range <>) of Character;
type Byte_Array_Ptr is access Byte_Array
with Storage_Pool => Pool;
-- A type that holds exactly one array on the heap and frees it when leaving the scope
type Buffer is new Ada.Finalization.Limited_Controlled with record
Data : Byte_Array_Ptr;
end record;
procedure Allocate (B : in out Buffer; Length : Positive);
overriding procedure Finalize (B : in out Buffer);
end Buffers;
with Ada.Unchecked_Deallocation;
package body Buffers is
procedure Free is new Ada.Unchecked_Deallocation (Byte_Array, Byte_Array_Ptr);
procedure Allocate (B : in out Buffer; Length : Positive) is
begin
Free (B.Data); -- If it already holds one, give it back first (does nothing if null)
B.Data := new Byte_Array (1 .. Length);
end Allocate;
overriding procedure Finalize (B : in out Buffer) is
begin
Free (B.Data);
end Finalize;
end Buffers;
with Counting_Pools; use Counting_Pools;
with Buffers; use Buffers;
procedure Pool_Demo is
begin
Report (Pool, "start ");
declare
X, Y : Buffer;
begin
Allocate (X, 100);
Allocate (Y, 200);
Allocate (X, 50); -- Give back X's 100 bytes, then take 50 bytes again
Report (Pool, "in block ");
end; -- Finalize for X and Y frees the rest
Report (Pool, "after block");
end Pool_Demo;
start : allocations = 0, deallocations = 0, in use = 0 bytes
in block : allocations = 3, deallocations = 1, in use = 268 bytes
after block: allocations = 3, deallocations = 3, in use = 0 bytes
Inside the block there were 3 allocations and 1 deallocation, and after leaving the block the counts of allocations and deallocations matched and usage was back to 0. Pool_Demo never calls Free. Deallocation happens only in the re-allocation inside Allocate and in Buffer’s Finalize. valgrind’s --leak-check=full also reported nothing for this program.
The 268 bytes in use are more than the total number of elements (250). Printing each size passed to Allocate gave 108, 208, and 60 bytes. The amount beyond the element count presumably includes space for holding the array’s index bounds, but the breakdown is up to the implementation and was not verified in this article.
flowchart TB
accTitle: Allocation and deallocation through the counting pool
accDescr: Shows that new and Free in the Buffer type call Allocate and Deallocate of the Counting_Pool specified for the access type, and Counting_Pool counts them and then leaves allocation and deallocation to GNAT's default pool.
N["new inside Buffer"] --> A["Allocate of Counting_Pool"]
FR["Free in Finalize"] --> D["Deallocate of Counting_Pool"]
A --> C["Count calls and usage"]
D --> C
C --> G["GNAT's default pool"]
G --> M["malloc and free"]
Figure 9: When you specify a pool for an access type, every new and Free for that type goes through the pool.
A pool that counts allocations like this can be used in tests to check that “usage returns to 0 after the operation.” GNAT also provides GNAT.Debug_Pools, which detects things like references to freed memory11. It was not tried in this article.
Subpools
Ada 2012 added System.Storage_Pools.Subpools, which divides one pool into multiple subpools12. You can specify the subpool to allocate from, as in new (Subpool) T, and deallocate a whole subpool at once. This suits uses such as throwing away, all at once at the end, the values allocated for each unit of processing. No subpool example was run in this article.
Reducing hand-written pointers with the standard containers
Combining access types and controlled types lets you build safe types, but you do not need to write your own every time you need a variable-length sequence or a dictionary. Ada.Containers, available since Ada 2005, has vectors, lists, maps, sets, and more13, and the containers handle allocating and deallocating elements.
Elements whose size differs from value to value, such as Strings of different lengths, go into the containers with Indefinite_ in their names.
with Ada.Containers.Indefinite_Vectors;
with Ada.Text_IO; use Ada.Text_IO;
procedure Containers_Demo is
-- Strings of different lengths can be elements as they are
package String_Vectors is new Ada.Containers.Indefinite_Vectors
(Index_Type => Positive, Element_Type => String);
Names : String_Vectors.Vector;
begin
Names.Append ("Ada");
Names.Append ("SPARK");
Names.Append ("GNAT");
for Name of Names loop
Put_Line (Name & ":" & Natural'Image (Name'Length));
end loop;
Names.Delete (2);
Put_Line ("length after delete:" & Ada.Containers.Count_Type'Image (Names.Length));
end Containers_Demo;
Ada: 3
SPARK: 5
GNAT: 4
length after delete: 2
This program has neither new nor Free. Run under valgrind, usage at exit was 0 bytes, and every allocated block had been freed.
==10040== in use at exit: 0 bytes in 0 blocks
==10040== All heap blocks were freed -- no leaks are possible
The containers are generic packages, so you instantiate them by passing the element type. How generics work is explained in “Generic Programming in Ada”.
Rules of thumb for choosing a mechanism
The mechanisms so far are easier to choose if you consider them in order from the top.
flowchart TB
accTitle: The order for choosing how to handle memory in Ada
accDescr: Shows the order of consideration: first, whether it can be declared as a value; use containers if elements are added and removed; access types and controlled types if a custom structure is needed; and storage pools if you want to decide where memory comes from or set a limit.
Q1{"Can it be declared and returned as a value?"} -->|"Yes"| V["Handle it as a variable or return value"]
Q1 -->|"No"| Q2{"Is a sequence or dictionary enough?"}
Q2 -->|"Yes"| C["Ada.Containers"]
Q2 -->|"No"| AC["Access types and controlled types"]
AC --> Q3{"Do you want to decide where memory comes from or set a limit?"}
Q3 -->|"Yes"| SP["Storage pools"]
Figure 10: If it cannot be handled as a value, consider containers; if those are not enough, consider access types and controlled types.
- If the only issue is that the length is decided at run time, declare it as a value and return it from functions. You do not need
new. - If you add and remove elements, use
Ada.Containers. - If you need a custom structure such as a tree or graph, build it with access types, and limit the places that call
Freeto a few, such as theFinalizeof a controlled type. Usealiasedandnot nullto write into the type what can be pointed to and whethernullis allowed. - When you want to decide the allocation limit or where memory comes from, specify
Storage_Sizeor a storage pool.
Even when Ada’s types express how memory is used, the compiler does not go as far as stopping misuse of Unchecked_Deallocation. Keep that part small, and check the remaining places with valgrind or a counting pool. For parts where you want the ownership rules checked as well, you can also write them in SPARK.
How to investigate memory that keeps growing in an environment with GC, such as .NET, is covered in “Telling GC Lag from a Memory Leak in .NET”.
Related articles
- The Appeal of the Ada Language — Expressing Design Through Types and Powering Software That Runs for Decades
- Generic Programming in Ada — Contracts, Types, and Zero-Cost Reuse
- Proving AI-Written Code with SPARK
- Connecting Ada with C and C# — Building a DLL and Calling It
- How Ada and Rust Think About Safety
- Practical Multithreading Best Practices: C++ Edition — Designing Failures Out with RAII and jthread
References
-
GNAT Reference Manual, System.Pool_Global (s-pooglo.ads). The same pool as the default storage pool, which uses malloc and free and does no automatic reclamation. ↩ ↩2 ↩3
-
Ada Reference Manual (Ada 2022), 3.10 Access Types. Pool-specific and general access types,
aliased, and null exclusion (not null). ↩ ↩2 ↩3 ↩4 -
GNAT User’s Guide, Switches for gnatbind. The role and implementation of the secondary stack, in the description of the
-Dswitch. ↩ ↩2 -
GNAT Reference Manual, No_Secondary_Stack. A restriction that guarantees at compile time that the generated code does not reference the secondary stack. ↩ ↩2
-
Ada Reference Manual (Ada 2022), 3.10.2 Operations of Access Types. Accessibility levels, and
Program_Errorwhen the check fails. ↩ ↩2 -
Ada Reference Manual (Ada 2022), 13.11.2 Unchecked Storage Deallocation. That
Xbecomes null afterFree (X), dangling references, and erroneous execution. ↩ ↩2 -
Annotated Ada Reference Manual (Ada 2022), 13.11.3 Default Storage Pools. 9.d/3 under “Wording Changes from Ada 2005”: that Ada 2012 removed the text about garbage collection and provided subpools. ↩
-
AdaCore, SPARK User’s Guide, Language Restrictions. That access types and allocators follow the ownership rules, and that only one access value at a time can read and write the allocated memory (Memory Ownership Policy). ↩
-
Ada Reference Manual (Ada 2022), 7.6 Assignment and Finalization.
ControlledandLimited_Controlled, the conditions under whichInitialize,Adjust, andFinalizeare called, and the steps of assignment. ↩ ↩2 -
Ada Reference Manual (Ada 2022), 13.11 Storage Management.
Root_Storage_PoolwithAllocate,Deallocate, andStorage_Size; the pool of a type withStorage_Sizespecified; andStorage_Error. ↩ ↩2 ↩3 ↩4 -
GNAT User’s Guide, The GNAT Debug Pool Facility.
GNAT.Debug_Pools, which checks on every dereference that the pointer points to properly allocated memory. ↩ -
Ada Reference Manual (Ada 2022), 13.11.4 Storage Subpools. Subpools with
System.Storage_Pools.Subpools. ↩ -
Ada Reference Manual (Ada 2022), A.18 Containers. The vectors, lists, maps, and sets of
Ada.Containers, and the containers withIndefinite_in their names. ↩
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Starting an Ada Project with Alire — Creating a Project, Adding Dependencies, Build Profiles, and GNATprove
Using Alire, the Ada package manager, to create a project, fetch GNAT automatically, add crates, switch build profiles, and prove code wi...
What's New in Ada 2022 — What Works in GNAT and the Parallel Syntax You Can't Write Yet
Declare expressions, delta aggregates, 'Image for all types, 'Reduce, Big_Integers and the Jorvik profile, run on GNAT 13.3 and 15.3. GNA...
Connecting Ada with C and C# — Building a DLL and Calling It
How to build a shared library in Ada and call it from C and C#, verified by running it: Interfaces.C, Convention => C, gprbuild, elaborat...
Generic Programming in Ada — Contracts, Types, and Zero-Cost Reuse
A systematic introduction to generic programming in Ada. Covers generic subprograms, generic packages, formal subprogram parameters, type...
The Appeal of the Ada Language — Expressing Design Through Types and Powering Software That Runs for Decades
An introduction to the appeal of the Ada language: strong typing, range constraints, separation of specification and implementation via p...
Related Topics
These are topic pages close to this theme. Starting from the article, you can go on to related services and other articles.
Windows Technical Topics
Entry point collecting technical topics on Windows development, bug investigation, and use of existing assets.
Services Connected to This Topic
This article leads to the following service pages. Start from the entry point closest to your situation.
Technical Consulting & Design Review
If you want to redesign which values go on the heap, and who frees them and when, in control or embedded code written in Ada or C, we can take this on as technical consulting and design review.
Bug Investigation & Root Cause Analysis
Investigating symptoms such as memory that keeps growing during long runs, or values that get corrupted because code reads memory that should already have been freed, is a good fit for bug investigation and root-cause analysis.
Frequently Asked Questions
Questions commonly asked at the time of consultation about the topic of this article.
- Does Ada have garbage collection?
- GNAT's default storage pool allocates and deallocates with malloc and free, and does not automatically reclaim memory that is no longer used. In the Ada 2012 revision, the text about garbage collection was removed from the Reference Manual, and subpools were added as a more predictable mechanism. Deallocate with Unchecked_Deallocation, or leave it to controlled types or the standard containers.
- Does a function that returns a string whose length is decided at run time use the heap?
- You can return it without writing new. Depending on the target, GNAT uses the secondary stack for functions that return arrays or records of unknown length. With GNAT 13.3 on Linux x86_64, returning a 1 MB string 10,000 times caused only a single heap allocation of about 1 MB. Adding pragma Restrictions (No_Secondary_Stack) makes such uses a compile error.
- After deallocating with Unchecked_Deallocation, what happens to other pointers to the same memory?
- The variable you passed to the deallocation becomes null, but other variables that pointed to the same memory keep the original address. Dereferencing them makes the execution erroneous (the result is not defined by the standard). When tried with GNAT 13.3, no exception was raised, and valgrind reported it as a read of freed memory.
- Does Ada have an equivalent of C++ RAII?
- Yes: the Controlled and Limited_Controlled types in Ada.Finalization. If you override Finalize, it is called when the object goes out of scope, including when leaving by an exception. If you write the heap deallocation there, the caller does not have to write it.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and bug investigation, with particular strength in projects where existing assets remain and in investigating failures whose cause is hard to see.