7.6 User-Defined Assignment and Finalization
[
{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}
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.]
Glossary entry: {Controlled type}
A controlled type supports user-defined assignment and finalization.
Objects are always finalized before being destroyed.
Ramification: {
AI95-00114-01}
{
AI95-00287-01}
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
The following language-defined
library package exists:
{
8652/0020}
{
AI95-00126-01}
package Ada.Finalization
is
pragma Preelaborate(Finalization);
pragma Remote_Types(Finalization);
{
AI95-00161-01}
type Controlled
is abstract tagged private;
pragma Preelaborable_Initialization(Controlled);
{
AI95-00348-01}
procedure Initialize (Object :
in out Controlled)
is null;
procedure Adjust (Object :
in out Controlled)
is null;
procedure Finalize (Object :
in out Controlled)
is null;
{
AI95-00161-01}
type Limited_Controlled
is abstract tagged limited private;
pragma Preelaborable_Initialization(Limited_Controlled);
{
AI95-00348-01}
procedure Initialize (Object :
in out Limited_Controlled)
is null;
procedure Finalize (Object :
in out Limited_Controlled)
is null;
private
... --
not specified by the language
end Ada.Finalization;
{
AI95-00348-01}
{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.
Discussion: We say “nonlimited
controlled type” (rather than just “controlled type”;
when we want to talk about descendants of Controlled only.
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.
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.
{
AI95-00251-01}
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.
{
AI95-00360-01}
A type is said to
need finalization if:
{needs
finalization} {type
(needs finalization)}
it is a controlled type, a task type or a protected
type; or
it has a component that needs finalization; or
it is a limited type that has an access discriminant
whose designated type needs finalization; or
it is one of a number of language-defined types
that are explicitly defined to need finalization.
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.
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
{
AI95-00373-01}
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.
Discussion:
Example:
type T1 is new Controlled with
record
... -- some components might have defaults
end record;
type T2 is new Controlled with
record
X : T1; -- no default
Y : T1 := ...; -- default
end record;
A : T2;
B : T2 := ...;
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.
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.
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.
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.
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.
{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:
The value of the target becomes the assigned value.
{adjusting the
value of an object} {adjustment}
The value of the target is
adjusted.
Ramification: If any parts of the object
are controlled, abort is deferred during the assignment operation.
{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.]
Ramification: Adjustment is never performed
for values of a by-reference limited type, since these types do not support
copying.
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.
{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”.]
Reason: An
alternative design for user-defined assignment might involve an Assign
operation instead of Adjust:
procedure Assign(Target : in out Controlled; Source : in out Controlled);
Or perhaps even
a syntax like this:
procedure ":="(Target : in out Controlled; Source : in out Controlled);
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
record
X : Array_Of_Controlled_Things(1..D);
case D is
when 17 => Y : Controlled_Thing;
when others => null;
end D;
end record;
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.
Forbidding such cases is not an option —
it would cause generic contract model violations.
Implementation Requirements
{
8652/0022}
{
AI95-00083-01}
{
AI95-00318-02}
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.
Reason: {
AI95-00318-02}
{
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;
...
private
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;
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).
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
An implementation is allowed to relax the above rules
[(for nonlimited controlled types)] in the following ways:
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.
For an
assignment_statement
that assigns to an object the value of that same object, the implementation
need not do anything.
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.
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.
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.
{
AI95-00147-01}
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).
Ramification: In the
aggregate
case, only one value adjustment is necessary, and there is no anonymous
object to be finalized.
{
AI95-00147-01}
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.
{
AI95-00147-01}
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:
any omitted Initialize call is not a call on a
user-defined Initialize procedure, and
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.
Reason: We don't want to eliminate objects
for which the Initialize might have side effects (such as locking a resource).
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
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
the assignment operations associated with omitted
Adjust calls are also omitted.
This permission applies to Adjust and Finalize calls
even if the implicit calls have additional external effects.
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
{
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
{
AI95-00161-01}
{
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
{
8652/0021}
{
AI95-00182-01}
Corrigendum: Added wording to clarify that the default initialization
(whatever it is) of an ancestor part is used.
{
8652/0022}
{
AI95-00083-01}
Corrigendum: Clarified that Adjust is never called on an aggregate
used for the initialization of an object or subaggregate, or passed as
a parameter.
{
AI95-00147-01}
Additional optimizations are allowed for nonlimited controlled types.
These allow traditional dead variable elimination to be applied to such
types.
{
AI95-00318-02}
Corrected the build-in-place requirement for controlled
aggregates
to be consistent with the requirements for limited types.
{
AI95-00348-01}
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).
{
AI95-00373-01}
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.