7.6 User-Defined Assignment and Finalization


7.6 User-Defined Assignment and Finalization

1 [{user-defined assignment} {assignment (user-defined)} Three kinds of actions are fundamental to the manipulation of objects: initialization, finalization, and assignment. Every object is initialized, either explicitly or by default, after being created (for example, by an object_declaration or allocator). Every object is finalized before being destroyed (for example, by leaving a subprogram_body containing an object_declaration, or by a call to an instance of Unchecked_Deallocation). An assignment operation is used as part of assignment_statements, explicit initialization, parameter passing, and other operations. {constructor: See initialization} {constructor: See Initialize} {destructor: See finalization} 

2 Default definitions for these three fundamental operations are provided by the language, but {controlled type} a controlled type gives the user additional control over parts of these operations. {Initialize} {Finalize} {Adjust} In particular, the user can define, for a controlled type, an Initialize procedure which is invoked immediately after the normal default initialization of a controlled object, a Finalize procedure which is invoked immediately before finalization of any of the components of a controlled object, and an Adjust procedure which is invoked as the last step of an assignment to a (nonlimited) controlled object.] 

2.a Glossary entry: {Controlled type} A controlled type supports user-defined assignment and finalization. Objects are always finalized before being destroyed.

2.b/2 Ramification: Here's the basic idea of initialization, value adjustment, and finalization, whether or not user defined: When an object is created, if it is explicitly assigned an initial value, the object is either built-in-place from an aggregate or function call (in which case neither Adjust nor Initialize is applied), or the assignment copies and adjusts the initial value. Otherwise, Initialize is applied to it (except in the case of an aggregate as a whole). An assignment_statement finalizes the target before copying in and adjusting the new value. Whenever an object goes away, it is finalized. Calls on Initialize and Adjust happen bottom-up; that is, components first, followed by the containing object. Calls on Finalize happen top-down; that is, first the containing object, and then its components. These ordering rules ensure that any components will be in a well-defined state when Initialize, Adjust, or Finalize is applied to the containing object. 

Static Semantics

3 The following language-defined library package exists: 


package Ada.Finalization is
    pragma Preelaborate(Finalization);
    pragma Remote_Types(Finalization);


    type Controlled is abstract tagged private;
    pragma Preelaborable_Initialization(Controlled);


    procedure Initialize (Object : in out Controlled) is null;
    procedure Adjust     (Object : in out Controlled) is null;
    procedure Finalize   (Object : in out Controlled) is null;


    type Limited_Controlled is abstract tagged limited private;
    pragma Preelaborable_Initialization(Limited_Controlled);


    procedure Initialize (Object : in out Limited_Controlled) is null;
    procedure Finalize   (Object : in out Limited_Controlled) is null;
    ... -- not specified by the language
end Ada.Finalization;

9/2 {controlled type} A controlled type is a descendant of Controlled or Limited_Controlled. The predefined "=" operator of type Controlled always returns True, [since this operator is incorporated into the implementation of the predefined equality operator of types derived from Controlled, as explained in 4.5.2.] The type Limited_Controlled is like Controlled, except that it is limited and it lacks the primitive subprogram Adjust. 

9.a Discussion: We say “nonlimited controlled type” (rather than just “controlled type”; when we want to talk about descendants of Controlled only. 

9.b Reason: We considered making Adjust and Finalize abstract. However, a reasonable coding convention is e.g. for Finalize to always call the parent's Finalize after doing whatever work is needed for the extension part. (Unlike CLOS, we have no way to do that automatically in Ada 95.) For this to work, Finalize cannot be abstract. In a generic unit, for a generic formal abstract derived type whose ancestor is Controlled or Limited_Controlled, calling the ancestor's Finalize would be illegal if it were abstract, even though the actual type might have a concrete version.

9.c Types Controlled and Limited_Controlled are abstract, even though they have no abstract primitive subprograms. It is not clear that they need to be abstract, but there seems to be no harm in it, and it might make an implementation's life easier to know that there are no objects of these types — in case the implementation wishes to make them “magic” in some way.

9.d/2 For Ada 2005, we considered making these types interfaces. That would have the advantage of allowing them to be added to existing trees. But that was rejected both because it would cause massive disruption to existing implementations, and because it would be very incompatible due to the "no hidden interfaces" rule. The latter rule would prevent a tagged private type from being completed with a derivation from Controlled or Limited_Controlled — a very common idiom. 

9.1/2 A type is said to need finalization if:{needs finalization} {type (needs finalization)} 

  • 9.2/2 it is a controlled type, a task type or a protected type; or
  • 9.3/2 it has a component that needs finalization; or
  • 9.4/2 it is a limited type that has an access discriminant whose designated type needs finalization; or
  • 9.5/2 it is one of a number of language-defined types that are explicitly defined to need finalization.

9.e/2 Ramification: The fact that a type needs finalization does not require it to be implemented with a controlled type. It just has to be recognized by the No_Nested_Finalization restriction.

9.f/2 This property is defined for the type, not for a particular view. That's necessary as restrictions look in private parts to enforce their restrictions; the point is to eliminate all controlled parts, not just ones that are visible.

Dynamic Semantics

10/2 During the elaboration or evaluation of a construct that causes an object to be initialized by default, for every controlled subcomponent of the object that is not assigned an initial value (as defined in 3.3.1), Initialize is called on that subcomponent. Similarly, if the object that is initialized by default as a whole is controlled, Initialize is called on the object.

11/2 For an extension_aggregate whose ancestor_part is a subtype_mark denoting a controlled subtype, the Initialize procedure of the ancestor type is called, unless that Initialize procedure is abstract. 

11.a Discussion: Example: 


type T1 is new Controlled with
        ... -- some components might have defaults
    end record;


type T2 is new Controlled with
        X : T1; -- no default
        Y : T1 := ...; -- default
    end record;


A : T2;
B : T2 := ...;

11.e As part of the elaboration of A's declaration, A.Y is assigned a value; therefore Initialize is not applied to A.Y. Instead, Adjust is applied to A.Y as part of the assignment operation. Initialize is applied to A.X and to A, since those objects are not assigned an initial value. The assignment to A.Y is not considered an assignment to A.

11.f For the elaboration of B's declaration, Initialize is not called at all. Instead the assignment adjusts B's value; that is, it applies Adjust to B.X, B.Y, and B.

11.f.1/2 The ancestor_part of an extension_aggregate, <> in aggregates, and the return object of an extended_return_statement are handled similarly. 

12 Initialize and other initialization operations are done in an arbitrary order, except as follows. Initialize is applied to an object after initialization of its subcomponents, if any [(including both implicit initialization and Initialize calls)]. If an object has a component with an access discriminant constrained by a per-object expression, Initialize is applied to this component after any components that do not have such discriminants. For an object with several components with such a discriminant, Initialize is applied to them in order of their component_declarations. For an allocator, any task activations follow all calls on Initialize. 

12.a Reason: The fact that Initialize is done for subcomponents first allows Initialize for a composite object to refer to its subcomponents knowing they have been properly initialized.

12.b The fact that Initialize is done for components with access discriminants after other components allows the Initialize operation for a component with a self-referential access discriminant to assume that other components of the enclosing object have already been properly initialized. For multiple such components, it allows some predictability. 

13 {assignment operation} When a target object with any controlled parts is assigned a value, [either when created or in a subsequent assignment_statement,] the assignment operation proceeds as follows: 
  • 14 The value of the target becomes the assigned value.
  • 15 {adjusting the value of an object} {adjustment} The value of the target is adjusted. 

15.a Ramification: If any parts of the object are controlled, abort is deferred during the assignment operation. 

16 {adjusting the value of an object} {adjustment} To adjust the value of a [(nonlimited)] composite object, the values of the components of the object are first adjusted in an arbitrary order, and then, if the object is controlled, Adjust is called. Adjusting the value of an elementary object has no effect[, nor does adjusting the value of a composite object with no controlled parts.] 

16.a Ramification: Adjustment is never performed for values of a by-reference limited type, since these types do not support copying. 

16.b Reason: The verbiage in the Initialize rule about access discriminants constrained by per-object expressions is not necessary here, since such types are limited, and therefore are never adjusted. 

17 {execution (assignment_statement) [partial]} For an assignment_statement, [ after the name and expression have been evaluated, and any conversion (including constraint checking) has been done,] an anonymous object is created, and the value is assigned into it; [that is, the assignment operation is applied]. [(Assignment includes value adjustment.)] The target of the assignment_statement is then finalized. The value of the anonymous object is then assigned into the target of the assignment_statement. Finally, the anonymous object is finalized. [As explained below, the implementation may eliminate the intermediate anonymous object, so this description subsumes the one given in 5.2, “Assignment Statements”.] 

17.a Reason: An alternative design for user-defined assignment might involve an Assign operation instead of Adjust: 

17.b procedure Assign(Target : in out Controlled; Source : in out Controlled);

17.c Or perhaps even a syntax like this: 

17.d procedure ":="(Target : in out Controlled; Source : in out Controlled);

17.e Assign (or ":=") would have the responsibility of doing the copy, as well as whatever else is necessary. This would have the advantage that the Assign operation knows about both the target and the source at the same time — it would be possible to do things like reuse storage belonging to the target, for example, which Adjust cannot do. However, this sort of design would not work in the case of unconstrained discriminated variables, because there is no way to change the discriminants individually. For example:


type Mutable(D : Integer := 0) is
        X : Array_Of_Controlled_Things(1..D);
        case D is
            when 17 => Y : Controlled_Thing;
            when others => null;
        end D;
    end record;

17.g An assignment to an unconstrained variable of type Mutable can cause some of the components of X, and the component Y, to appear and/or disappear. There is no way to write the Assign operation to handle this sort of case.

17.h Forbidding such cases is not an option — it would cause generic contract model violations. 

Implementation Requirements

17.1/2 For an aggregate of a controlled type whose value is assigned, other than by an assignment_statement, the implementation shall not create a separate anonymous object for the aggregate. The aggregate value shall be constructed directly in the target of the assignment operation and Adjust is not called on the target object. 

17.h.1/2 Reason: {build-in-place [partial]} This build-in-place requirement is necessary to prevent elaboration problems with deferred constants of controlled types. Consider: 


package P is
   type Dyn_String is private;
   Null_String : constant Dyn_String;
   type Dyn_String is new Ada.Finalization.Controlled with ...
   procedure Finalize(X : in out Dyn_String);
   procedure Adjust(X : in out Dyn_String);

   Null_String : constant Dyn_String :=
      (Ada.Finalization.Controlled with ...);
end P;

17.h.3/1 When Null_String is elaborated, the bodies of Finalize and Adjust clearly have not been elaborated. Without this rule, this declaration would necessarily raise Program_Error (unless the permissions given below are used by the implementation). 

17.i/2 Ramification: An aggregate used in the return expression of a simple_return_statement has to be built-in-place in the anonymous return object, as this is similar to an object declaration. (This is a change from Ada 95, but it is not an inconsistency as it only serves to restrict implementation choices.) But this only covers the aggregate; a separate anonymous return object can still be used unless it too is required to be built-in-place (see 7.5). 

Implementation Permissions

18 An implementation is allowed to relax the above rules [(for nonlimited controlled types)] in the following ways: 

18.a Proof: The phrase “for nonlimited controlled types” follows from the fact that all of the following permissions apply to cases involving assignment. It is important because the programmer can count on a stricter semantics for limited controlled types.

  • 19 For an assignment_statement that assigns to an object the value of that same object, the implementation need not do anything. 

19.a Ramification: In other words, even if an object is controlled and a combination of Finalize and Adjust on the object might have a net side effect, they need not be performed. 

  • 20 For an assignment_statement for a noncontrolled type, the implementation may finalize and assign each component of the variable separately (rather than finalizing the entire variable and assigning the entire new value) unless a discriminant of the variable is changed by the assignment. 

20.a Reason: For example, in a slice assignment, an anonymous object is not necessary if the slice is copied component-by-component in the right direction, since array types are not controlled (although their components may be). Note that the direction, and even the fact that it's a slice assignment, can in general be determined only at run time. 

  • 21/2 For an aggregate or function call whose value is assigned into a target object, the implementation need not create a separate anonymous object if it can safely create the value of the aggregate or function call directly in the target object. Similarly, for an assignment_statement, the implementation need not create an anonymous object if the value being assigned is the result of evaluating a name denoting an object (the source object) whose storage cannot overlap with the target. If the source object might overlap with the target object, then the implementation can avoid the need for an intermediary anonymous object by exercising one of the above permissions and perform the assignment one component at a time (for an overlapping array assignment), or not at all (for an assignment where the target and the source of the assignment are the same object).

21.a Ramification: In the aggregate case, only one value adjustment is necessary, and there is no anonymous object to be finalized.

21.b/2 Similarly, in the function call case, the anonymous object can be eliminated. Note, however, that Adjust must be called directly on the target object as the last step of the assignment, since some of the subcomponents may be self-referential or otherwise position-dependent. This Adjust can be eliminated only by using one of the following permissions.

22/2 Furthermore, an implementation is permitted to omit implicit Initialize, Adjust, and Finalize calls and associated assignment operations on an object of a nonlimited controlled type provided that:

  • 23/2 any omitted Initialize call is not a call on a user-defined Initialize procedure, and 

23.a/2 To be honest: This does not apply to any calls to a user-defined Initialize routine that happen to occur in an Adjust or Finalize routine. It is intended that it is never necessary to look inside of an Adjust or Finalize routine to determine if the call can be omitted. 

23.b/2 Reason: We don't want to eliminate objects for which the Initialize might have side effects (such as locking a resource). 

  • 24/2 any usage of the value of the object after the implicit Initialize or Adjust call and before any subsequent Finalize call on the object does not change the external effect of the program, and
  • 25/2 after the omission of such calls and operations, any execution of the program that executes an Initialize or Adjust call on an object or initializes an object by an aggregate will also later execute a Finalize call on the object and will always do so prior to assigning a new value to the object, and
  • 26/2 the assignment operations associated with omitted Adjust calls are also omitted.

27/2  This permission applies to Adjust and Finalize calls even if the implicit calls have additional external effects. 

27.a/2 Reason: The goal of the above permissions is to allow typical dead assignment and dead variable removal algorithms to work for nonlimited controlled types. We require that “pairs” of Initialize/Adjust/Finalize operations are removed. (These aren't always pairs, which is why we talk about “any execution of the program”.) 

Extensions to Ada 83

27.b {extensions to Ada 83} Controlled types and user-defined finalization are new to Ada 95. (Ada 83 had finalization semantics only for masters of tasks.) 

Extensions to Ada 95

27.c/2 {extensions to Ada 95} Amendment Correction: Types Controlled and Limited_Controlled now have Preelaborable_Initialization, so that objects of types derived from these types can be used in preelaborated packages. 

Wording Changes from Ada 95

27.d/2 Corrigendum: Clarified that Ada.Finalization is a remote types package.

27.e/2 Corrigendum: Added wording to clarify that the default initialization (whatever it is) of an ancestor part is used.

27.f/2 Corrigendum: Clarified that Adjust is never called on an aggregate used for the initialization of an object or subaggregate, or passed as a parameter.

27.g/2 Additional optimizations are allowed for nonlimited controlled types. These allow traditional dead variable elimination to be applied to such types.

27.h/2 Corrected the build-in-place requirement for controlled aggregates to be consistent with the requirements for limited types.

27.i/2 The operations of types Controlled and Limited_Controlled are now declared as null procedures (see 6.7) to make the semantics clear (and to provide a good example of what null procedures can be used for).

27.j/2 Types that need finalization are defined; this is used by the No_Nested_Finalization restriction (see D.7, “Tasking Restrictions”).

27.k/2 Generalized the description of objects that have Initialize called for them to say that it is done for all objects that are initialized by default. This is needed so that all of the new cases are covered.

Syndicate content