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" &
…