Version 1.1 of ai05s/ai05-0142-2.txt
!standard 3.10(6) 09-04-03 AI05-0142-2/01
!standard 3.10(12/2)
!standard 3.10.2(13.1/2)
!standard 3.10.2(18.1/2)
!class Amendment 09-04-04
!status work item 09-04-04
!status received 09-03-19
!priority Medium
!difficulty Medium
!subject Limited access types
!summary
(See proposal.)
!problem
Modifying a portion of a larger opaque object (such as a container) is not
well-supported in Ada. The Ada.Containers packages provide a procedure
Replace_Element for this purpose. But this procedure requires copying the
element (potentially in both directions). That could be very expensive if the
element is large.
The Ada.Containers packages also provide an procedure Update_Element for this
purpose; this provides a writable object as a parameter to a subprogram passed
into the procedure. This procedure avoids the need to copy the element, but it
is hardly convenient to define a procedure for every component of the element
that needs changing. The extra syntax needed obscures the real meaning of the
program.
An option that was rejected for the Ada.Containers packages was to return an
access to the element type. However, this is problematic as it is difficult to
control the accessibility and lifetime of the returned access. If the element is
removed from the container, the returned access could become dangling; continued
use of the access would make the program erroneous. Moreover, the accessibility
of the returned object (and thus what could be done with it) would depend on the
actual implementation of the container. Bounded containers would typically only
return access values with a very short lifetime, while unbounded containers
would typically return access values with a much longer lifetime. Converting
from an unbounded to bounded form could thus introduce new runtime errors - a
serious maintenance hazard.
!proposal
Add "limited" to anonymous access types. This would prevent the access value from
being copied to another access type -- essentially, it can only be dereferenced.
We accomplish this by defining that limited anonymous access types have the same
sort of accessibility as an anonymous access-to-subprogram parameter.
limited anonymous access types always have Storage_Size = 0.
Special case: The master of the parameters of a function with a limited access result
would have be the same as the call as a whole. That would allow returning part of
one of the parameters as the access result (without the parameter disappearing
prematurely).
For the purposes of converting to a limited access type, accessibility would be
checked as follows: [I don't know off-hand how to word this, the only existing
cases that I know of allow anything to be converted to the type. - RLB]
* For a limited access parameter, any accessibility is allowed.
* For a limited access result, the converted access would have to have
a lifetime at least as long as the function call. This would include its
parameters, but not any local object. Special case: A limited access parameter
(or part thereof) can be returned as a limited access result. [This works because
the parameter has to exist at least as long as the call does: recall the special
master rule noted above. And it can't be assigned or converted beyond that.]
[RLB: We would like to allow a function with a limited access result to
return the result of a call to another function with a limited access result. But
that is not safe given the rules as given here. Imagine:
function B (P : access Something) return limited access Something is
begin
return P;
end B;
function A return limited access Something is
begin
declare
Local : aliased Something;
begin
return Local'access; --
return B (Local'access); --
end;
end A;
The problem here is B has "filed-off" the accessibility of the parameter,
and we now have to assume the worst. Note that making P a limited access
doesn't change anything. I don't see any solution to this problem, and
since it is not critical to solving the original problem, I simply didn't
try.]
* For a limited access type used as the type of a component or stand-alone object,
the converted access would have to have the same or greater lifetime than the
object. Special case: For a limited access type used as a component of
a return object, we use the accessibility that the object will have
after the return (not the one that is current inside of the return statement).
Otherwise, we'd again allow local objects. Note that this is the same rule
as used for limited access results. [RLB: An alternative would be to
recheck the accessibility when the accessibility of the return object changes.
But that sounds like a pain to implement.]
[RLB: Note that 3.10.2(10/2) means that we could initialize a stand-alone object
with a function returning a limited access type. But that isn't allowed
"naturally" because a limited access has an infinite accessibility. Do we
need a special-case rule to handle that case?? OTOH, we surely do not
want to allow any other initialization of a stand-alone object with
a limited access.]
* For a limited access type used as the type of a formal object, the converted
access would have to have the same or greater lifetime than the instance.
[Not sure if this is actually different than the above.]
* For a limited access type used as the type of an object renaming, the converted
access would have to have the same or greater lifetime than the renaming.
In the case where a function call with a limited access result is being
renamed, we would not be any accessibility check. (In this case, the master
of the function call is that of the renames, so everything will be OK.)
* For a limited access type used as the type of an access discriminant, ????
[Do we need the entire mess of accessibility for access discriminants,
or is the admittedly messy rules for limited access components already
cover what we need to allow? That is, we "just" use the accessibility of
the object. I lean toward unifying them, but I'm not certain. Note that
no coextensions are allowed, because there aren't any allocators.]
!wording
Replace 3.10(6) by:
access_definition ::=
[null_exclusion] [limited] access [constant] subtype_mark
[null_exclusion] [limited] access [protected] procedure parameter_profile
[null_exclusion] [limited] access [protected] function parameter_and_result_profile
Add after 3.10(12/2):
An access_definition that contains the reserved word *limited* is called a *limited
access type*. In addition, the anonymous access subtype defined by the access_definition
of a parameter is a limited access type if the access_definition defines an
access-to-subprogram type.
[RLB: should this term include "anonymous" since we are only going to define
these for anonymous access types? That is, "limited anonymous access type".]
The Storage_Size of a limited access type is defined to be zero.
[RLB: This is intended to trigger 4.8(5.3/2). Note that we can't actually
write the Storage_Size attribute for these types, so I can't steal the wording
of E.2.2(17/2).]
Replace 3.10.2(13.1/2):
* The accessibility level of a limited access type is deeper than that of any master;
all limited access types have this same level.
[RLB: We probably ought change all of the other existing uses of
"anonymous access type" in this list of bullets to say "non-limited anonymous
access type" to avoid ambiguity, and also in 3.10.2(19/2). That's annoying.]
Replace 3.10.2(18.1/2):
* The accessibility level of a limited access type is statically deeper than that
of any master; all limited access types have this same level,
** Rest TBD **
!discussion
This proposal grew out of discussions about the problems noted in AI05-0142-1;
what usually is wanted is an access value that can be dereferenced but not
converted to some other type.
We only allow anonymous limited access types as it would be barely useful to
allocate objects for a type that couldn't be copied. Moreover, the implicit
conversions of anonymous access is exactly what is needed: conversions in
are automatic, and conversions out are illegal anyway, so there is no problem
that they are harder than usual to define.
We do include anonymous access-to-subprogram types, as it seems that they could
be useful as well -- besides, we invented the idea for them originally.
Allocators are not allowed for limited anonymous access types. We could try to
define their accessibility (it would be similar to what is allowed to be converted),
but it doesn't seem very useful, as the object could only be dereferenced
afterwards. Moreover, it seems difficult to avoid storage leaks. Finally, user
experience with the existing allocators of anonymous access types is that they
are usually exactly the wrong thing, so we don't see any compelling reason to
make this problem worse with a new feature.
The intent of this feature is to use it in cases where it is desired to be able
to return a writable reference to an object, but where creating a first-class
access type is undesirable (because of lifetime issues, perhaps).
This feature directly cannot solve all dangling access issues, but it can be
combined with other existing Ada features to do so.
For instance, in the containers libraries, it would be tempting to define:
function Modifiable_Element (Container : in out Vector;
Position : in Cursor) return limited access Element_Type;
However, this would not be safe if the element was deleted from the container while
the limited access object still exists. It might appear that this is unlikely, but
it would be easy to rename the result of one of these calls or pass it's designated
object as the parameter of a complex subprogram. Once the usage is sufficiently
separated from the original call, it no longer would be unlikely for some modification
to be performed to the container. (Recall that since we allow "sliding" of elements
in vectors, deleting any element from a vector would change the element accessed,
possibly leading to erroneous execution if a discriminant constraint had been applied
somewhere.)
But we can combine this feature with finalization of objects to handle the problem properly
(see the examples below).
!examples
In the existing containers libraries, we need to be able to treat the entire existence of
a limited access to an element to be program text where tampering with elements is prohibited.
The tricky part is getting a callback to clear the tampering state when the limited access no
longer exists, since we don't have (non-trivial) finalization of access values.
We can accomplish this callback by wrapping the limited access in a controlled object. This
requires two types in Ada as we currently have it defined, one for the private tampering
mechnism, and one for the public limited access:
type I_Tamper_With_Elements is tagged limited private;
type Element_Accessor is limited new I_Tamper_With_Elements with record
Element : limited access Element_Type;
end record;
function Modifiable_Element (Container : in out Vector;
Position : in Cursor) return Element_Accessor;
Note that the name "Element" has already been used by the containers libraries,
and it is fact was used for a function with a type-conforming profile, so using
that name would be inadvisable. Moreover, the fact that the container is "in
out" when it can be modified (a standard design feature of the containers
libraries) suggests that a different name be used so that it is still possible
to read "in" parameter containers.
This could be used (based on a recent example from comp.lang.ada):
with Ada.Containers.Vectors;
procedure Check is
package Integer_Vectors is
new Ada.Containers.Vectors
(Index_Type => Natural,
Element_Type => Integer);
package Nested_Vectors is
new Ada.Containers.Vectors
(Index_Type => Natural,
Element_Type => Integer_Vectors.Vector,
"=" => Integer_Vectors."=");
IV : Integer_Vectors.Vector;
NV : Nested_Vectors.Vector;
begin
IV.Append(42);
NV.Append(IV);
NV.Modifiable_Element(0).Append(43);
end Check;
The dereference does not need to be explicitly given in this case, and the fact that
the object returned by Modifiable_Element is limited and (probably) controlled is not visible
in the code.
You could also save the entire object as long as needed:
declare
My_IV : Nested_Vectors.Element_Accessor := NV.Modifiable_Element(0);
begin
(since My_NV would be built-in-place).
Note that this design still works as intended even if the user renames only the limited access value:
declare
My_IV : limited access Integer_Vectors.Vector renames NV.Modifiable_Element(0).all;
begin
Even in this case, the wrapping finalizable object still is protecting the container, because
the master of the returned object is the master of the renames: it will stick around as long
as the renames does.
Moreover, splitting the access from the object usually isn't possible:
declare
My_IV : limited access Integer_Vectors.Vector := NV.Modifiable_Element(0).all; --
begin
This is illegal because the accessibility check fails. (And that's intentional!)
It is OK to pass this element as a parameter, as again the wrapping object will live as long as
the parameter:
Operate (NV.Modifiable_Element(0));
or
Munge (NV.Modifiable_Element(0).all);
Claw uses a very similar scheme to ensure that an object which has been returned cannot become
dangling because of the actions of another task. Since Claw is written in Ada 95, it could not
prevent the client from copying the access and defeating the purpose of the lock object. But
the limited access type would prevent that problem.
Aside: It would be nice in this case if we could avoid the extra type in order to have a
partially visible type. But I don't think this case is common enough to justify the
violence to the language semantics that would be needed. Especially given the confusion
that would ensue given that a partial view is of a private type, so some other description
would have to be given to a partially private type.
!ACATS test
ACATS tests would be needed for this feature.
!appendix
****************************************************************
Questions? Ask the ACAA Technical Agent