Gem #150: Out and Uninitialized
by Robert Dewar —AdaCore
Perhaps surprisingly, the Ada standard indicates cases where objects passed to out and in out parameters might not be updated when a procedure terminates due to an exception. Let's take an example:
with Ada.Text_IO; use Ada.Text_IO; procedure Gem is procedure Local (A : in out Integer; Error : Boolean) is begin A := 1; if Error then raise Program_Error; end if; end Local; B : Integer := 0; begin Local (B, Error => True); exception when Program_Error => Put_Line ("Value for B is" & Integer'Image (B)); -- "0" end Gem;
This program outputs a value of 0 for B, whereas the code indicates that A is assigned before raising the exception, and so the reader might expect B to also be updated.
The catch, though, is that a compiler must by default pass objects of elementary types (scalars and access types) by copy and might choose to do so for other types (records, for example), including when passing out and in out parameters. So what happens is that while the formal parameter A is properly initialized, the exception is raised before the new value of A has been copied back into B (the copy will only happen on a normal return).
In general, any code that reads the actual object passed to an out or in out parameter after an exception is suspect and should be avoided. GNAT has useful warnings here, so that if we simplify the above code to:
with Ada.Text_IO; use Ada.Text_IO; procedure Gem2 is procedure Local (A : in out Integer) is begin A := 1; raise Program_Error; end Local; B : Integer := 0; begin Local (B); exception when others => Put_Line ("Value for B is" & Integer'Image (B)); end Gem2;
We now get a compilation warning:
gem.adb:6:10: warning: assignment to pass-by-copy formal may have no effect gem.adb:6:10: warning: "raise" statement may result in abnormal return (RM 6.4.1(17))
Of course, GNAT is not able to point out all such errors (see first example above), which in general would require full flow analysis.
The behavior is different when using parameter types that the standard mandates passing by reference, such as tagged types for instance. So the following code will work as expected, updating the actual parameter despite the exception:
procedure Gem3 is type Rec is tagged record Field : Integer; end record; procedure Local (A : in out Rec) is begin A.Field := 1; raise Program_Error; end Local; V : Rec; begin V.Field := 0; Local (V); exception when others => Put_Line ("Value of Field is" & V.Field'Img); -- "1" end Gem3;
It's worth mentioning that GNAT provides a pragma called Export_Procedure that forces reference semantics on out parameters. Use of this pragma would ensure updates of the actual parameter prior to abnormal completion of the procedure. However, this pragma only applies to library-level procedures, so the examples above have to be rewritten to avoid the use of a nested procedure, and really this pragma is intended mainly for use in interfacing with foreign code. The code below shows an example that ensures that B is set to 1 after the call to Local:
package Gem4_Support is procedure Local (A : in out Integer; Error : Boolean); pragma Export_Procedure (Local, Mechanism => (A => Reference)); end Gem4_Support;
package body Gem4_Support is procedure Local (A : in out Integer; Error : Boolean) is begin A := 1; if Error then raise Program_Error; end if; end Local; end Gem4_Support;
with Ada.Text_IO; use Ada.Text_IO; with Gem4_Support; use Gem4_Support; procedure Gem4 is B : Integer := 0; begin Local (B, Error => True); exception when Program_Error => Put_Line ("Value for B is" & Integer'Image (B)); -- "1" end Gem4;
In the case of direct assignments to global variables, the behavior in the presence of exceptions is somewhat different. For predefined exceptions, most notably Constraint_Error, the optimization permissions allow some flexibility in whether a global variable is or is not updated when an exception occurs (see Ada RM 11.6). For instance, the following code makes an incorrect assumption:
X := 0; -- about to try addition Y := Y + 1; -- see if addition raises exception X := 1 -- addition succeeded
A program is not justified in assuming that X = 0 if the addition raises an exception (assuming X is a global here). So any such assumptions in a program are incorrect code which should be fixed.