Controlled Types: Initialize, Adjust, Finalize (RAII)
Derive from Ada.Finalization.Controlled and Limited_Controlled to run code on object creation, copy and destruction, implement deep copy in Adjust, free resources in Finalize (idempotently), and use scope-based resource management that works even when exceptions propagate.
Controlled types give Ada constructors, copy hooks and destructors. Derive from Ada.Finalization.Controlled and override Initialize (called when an object is default-initialized), Adjust (called after an assignment copies the object) and Finalize (called when the object is destroyed or overwritten). Finalize runs even when a scope is left by an exception, which gives the RAII (resource acquisition is initialization) pattern.
The Three Hooks
Initialize runs for objects declared without an explicit initial value. Assignment 'B := A' for controlled types is: Finalize the old value of B, copy A's bits into B, then Adjust (B) — so Adjust is where you turn a shallow copy into a deep copy (allocate new heap storage and copy the contents). Finalize runs when an object goes out of scope, is overwritten, or is freed; objects are finalized in reverse order of creation. Initialization with a value ('B : Buffer := A') calls Adjust, not Initialize.
• Initialize — default initialization. • Adjust — after copy (deep copy here). • Finalize — on scope exit, overwrite or deallocation; reverse order. • Finalize must be idempotent (safe to run twice).
Limited_Controlled and RAII
Ada.Finalization.Limited_Controlled has Initialize and Finalize but no Adjust (limited objects can't be copied). It is perfect for owning resources: a lock guard that releases in Finalize, a file wrapper that closes, a transaction that rolls back unless committed. Because Finalize runs during exception propagation, cleanup is guaranteed. Discriminants (here an access-to-constant String) can pass parameters to the guard. Standard library examples: Unbounded_String and the containers are controlled.
• Limited_Controlled: Initialize + Finalize only. • Finalize runs on exceptions too → guaranteed cleanup. • Use for locks, files, handles, transactions.
Code Examples
A is default-initialized (Initialize). B:= A copies and Adjust makes a deep copy, so changing B.Data doesn't affect A. Objects are finalized as their scopes end, inner first. The Scoped_Log guard prints its leave message even though the block raises Program_Error. Output: Declare A: Initialize buffer 1 Declare B:= A: Adjust (deep copy of 1) -> 2 A.Data = 5, B.Data = 99 Leaving B's scope: Finalize buffer 2 Leaving A's scope: Finalize buffer 1 RAII with Limited_Controlled: >> enter work doing work, then raising an exception... << leave work Handled Program_Error (Finalize already ran) -- controlled type holding a pointer, no Adjust overriding procedure Adjust (B: in out Buffer); -- deep copy
Without Adjust both copies share the heap object, and both Finalize calls free it (double free).
procedure Finalize (B: in out Buffer) is begin Free (B.Data); -- assumes non-null, runs twice if B.Data /= null then Free (B.Data); end if;
Finalize may be called more than once; make it idempotent.
type Buffer is new Ada.Finalization.Controlled with... -- declared in a subprogram -- declare controlled types at library level (in a package)
Controlled type extensions must not be more nested than their parent, so declare them in library packages.
Which hook runs after an assignment copies a controlled object? A. Initialize B. Adjust C. Finalize D. Copy
Does Finalize run when a scope is left because of an exception? A. Yes B. No C. Only for Limited_Controlled D. Only with pragma
A. Yes (verified: 'leave work' printed before the handler).
Which type has no Adjust? A. Controlled B. Limited_Controlled C. Both have it D. Neither B. Limited objects are never copied.
In what order are objects in a scope finalized? A. Declaration order B. Reverse order of creation C. Random D. Alphabetical Trace the controlled-type example.
Real output: Declare A: Initialize buffer 1 Declare B := A: Adjust (deep copy of 1) -> 2 A.Data = 5, B.Data = 99 Leaving B's scope: Finalize buffer 2 Leaving A's scope: Finalize buffer 1 RAII with Limited_Controlled: >> enter work doing work, then raising an exception... << leave work Handled Program_Error (Finalize already ran)
-- file: tracked.ads
with Ada.Finalization;
package Tracked is
type Int_Access is access all Integer;
-- Controlled: user hooks run on creation, copy and destruction
type Buffer is new Ada.Finalization.Controlled with record
Id : Natural := 0;
Data : Int_Access;
end record;
overriding procedure Initialize (B : in out Buffer);
overriding procedure Adjust (B : in out Buffer);
overriding procedure Finalize (B : in out Buffer);
-- Limited_Controlled: RAII for resources that cannot be copied
type Scoped_Log (Name : access constant String) is
new Ada.Finalization.Limited_Controlled with null record;
overriding procedure Initialize (S : in out Scoped_Log);
overriding procedure Finalize (S : in out Scoped_Log);
end Tracked;
-- file: tracked.adb
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Unchecked_Deallocation;
package body Tracked is
procedure Free is new Ada.Unchecked_Deallocation (Integer, Int_Access);
Next_Id : Natural := 0;
overriding procedure Initialize (B : in out Buffer) is
begin
Next_Id := Next_Id + 1;
B.Id := Next_Id;
B.Data := new Integer'(0);
Put_Line (" Initialize buffer" &
…