!topic LSN on Access Discriminants in Ada 9X !key LSN-1050 on Access Discriminants in Ada 9X !reference MS-3;4.6 !reference LSN-1033 !from Bob Duff $Date: 92/11/04 14:50:28 $ $Revision: 1.5 $ !discussion This Language Study Note discusses access discriminants in Ada 9X. First, I show a simple example to illustrate the feature. Then, I discuss the semantics of the feature, and contrast it with other features (such as plain old record components that happen to be of an access type). Finally, I give several more examples, to show various situations in which access discriminants are necessary or useful. ITERATOR EXAMPLE: The following package exports a Set abstraction, with an Iter type used to iterate over the elements of a given Set. generic type Element is private; package Sets is type Set is tagged limited private; -- sets of elements ... -- various set operations type Iter(S: access Set) is limited private; function Done(I: Iter); procedure Get_Next_Element(I: in out Iter; E: out Elem); private ... -- implementation details end Sets; package Int_Sets is new Sets(Element => Integer); with Int_Sets; use Int_Sets; procedure P(S: Set) is I: Iter(S'Access); begin while not Done(I) loop declare E: Elem; begin Get_Next_Element(I, E); ... -- operate on E end; end loop; end P; Note that the iterator contains a reference to the object it is iterating over; we don't want it to contain a copy of that object. DISCUSSION OF SEMANTICS: In Ada 9X, an access discriminant is declared like this: type T is ...; type Rec(D: access T) is limited record ... end record; D is an access discriminant, which means that it is of an anonymous access type whose designated type is T. An access discriminant of an object can point at another object that is declared in the same scope, or any containing scope. This feature is needed when an object needs to contain a reference to an object in the same scope (or more generally, in some scope that is more deeply nested than the type of the referencing object). For example: procedure P is X: aliased T := ...; Y: Rec(X'Access); begin ... end P; Access discriminants are only allowed for limited types. Why can't we simply use a component that is of an access type in the same way? For two reasons: (1) the accessibility requirements, which are meant to prevent dangling pointers, would forbid it, and (2) if the type is limited, there is no way to initialize the reference at the point of declaration. The following examples illustrate these two reasons: package Library_Pack is type T is ...; type A is access T; Global: A; end Library_Pack; with Library_Pack; use Library_Pack; procedure P is X: aliased T := ...; Local: A := X'Access; -- Illegal! begin Global := X'Access; -- Illegal! Global := Local; ... end P; The statement "Global := X'Access" is illegal for obvious reasons: it creates a pointer in a library-level object to a more-nested object; the more-nested object will disappear when P returns, leaving a dangling pointer. But, since access values are freely assignable, a program might create a local access object, and only later assign it to a global, as illustrated by the second assignment to Global. Therefore, we need to forbid the creation of the access value in the first place. The accessibility rules do just that. Here's how: The result of both of the X'Accesses above is type A. But X is more nested than A, and the accessibility rules forbid 'Access when the object is more nested than the access type. Note that if A were declared INSIDE P, then the two X'Accesses above would be legal. But then it would be impossible to declare Global, so there would be no problem. Now, suppose we have a record component of type A: package Library_Pack is type T is ...; type A is access T; type Rec is record D: A; ... end record; Global_Rec: Rec; end Library_Pack; with Library_Pack; use Library_Pack; procedure P is X: aliased T := ...; Local_Rec: Rec := (D => X'Access, -- Illegal! ...); begin Global_Rec := Local_Rec; ... end P; The same problem occurs. We could get around the problem by using X'Unchecked_Access, but then we lose the compile-time checks that prevent dangling pointers. Furthermore, suppose Rec needs to be limited for some reason. (There are many reasons to use limited types in Ada 9X.) Then, it is illegal to give an explicit initial value for Local. The ability to initialize objects at their declaration point is an important aid to program readability. Access discriminants behave differently (with respect to the accessibility rules) than normal components of an access type. In particular, the anonymous type of the access parameter is considered to be declared INSIDE each individual object. package Library_Pack is type T is ...; type A is access T; type Rec(D: access T) is limited record ... end record; Global: A; Global_Rec: Rec(...); end Library_Pack; with Library_Pack; use Library_Pack; procedure P is X: aliased T := ...; Local_Rec: Rec(D => X'Access); -- OK now. begin Global := Local_Rec.D; -- Illegal (type mismatch)! Global := A(Local_Rec.D); -- Illegal (accessibility rule)! Global_Rec := Local_Rec; -- Illegal (assign of limited)! ... end P; Now that D is an access discriminant, it is possible to make it point to a local object. Any attempt to assign that pointer to a global will be illegal. There are two reasons this works for access discriminants: - Discriminants are constant. (In fact in an earlier version of Ada, 1980 or 1981, discriminants were called "constant components", and their declarations looked like constant declarations.) This means it is impossible to assign to the D component of a global Rec. - Access discriminants are only allowed for limited types. Thus, there is no way to do a whole-record assignment to change the D component of a global Rec. The set iterator shown above was an example where a reference to a local object was necessary. Here are some more examples: LOCKING EXAMPLE: In this example, we use initialization and finalization to ensure that a data structure is properly locked and unlocked: type Handle(Resource: access Some_Resource_Type) is new Controlled with null; procedure Initialize(H: Handle) is begin Lock(H.Resource); end Initialize; procedure Finalize(H: Handle) is begin Unlock(H.Resource); end Finalize; ... procedure P(R: Some_Resource_Type) is H: Handle(R'Access); -- lock R begin ... -- process the locked resource end P; The declaration of H in P ensures that R will be locked during the execution of P, and that no matter how P is left (normally, because of an exception, etc.), R will be unlocked. This is the recommended method in C++ for doing locking/unlocking in a safe manner, given the recent addition of exceptions to C++ (which already had finalization). It works in Ada 9X because the handle H can contain a reference to the resource R. Similar examples involve opening/closing a file, allocating/deallocating an instance of some resource, such as heap space, and starting/stopping a database transaction. WORK DESCRIPTOR FOR TASK: The next example uses an access discriminant to give a task a reference to a "work descriptor": type Work_Descriptor is record ... -- description of work to be done end record; task type T(Work: access Work_Descriptor) is entry Get_Result(Result: out ...); end T; task body T is begin ... -- refer to components of Work here. accept Get_Result(Result: out ...) do Result := ...; end Get_Result; end T; ... This_Work: aliased Work_Descriptor := (...); This_Task: T(This_Work'Access); That_Work: aliased Work_Descriptor := (...); That_Task: T(That_Work'Access); ... This_Task.Get_Result(...); That_Task.Get_Result(...); PROTECTION OF A RESOURCE: The next example shows a protected type that contains a reference to the resource it is protecting: protected type Prot(R: access Resource) is procedure Manipulate; entry Grind; end Prot; protected body Prot is procedure Manipulate is begin ... -- refer (safely) to R.all here end Manipulate; entry Grind when R.Some_Field >= 0 is begin ... -- refer (safely) to R.all here end Grind; end Prot; P: Prot(My_Resource'Access); ... P.Manipulate; P.Grind; REFERENCE TO CONTAINING OBJECT: LSN-1033 discusses cases where a component needs a reference to its containing object. We had originally thought that the "multiple inheritance" mechanism of LSN-1033 would be useful for adding finalization to a type that has none. We no longer think this is useful in practice, but it does work, and the following example illustrates the technique. LSN-1033 shows examples where the technique actually IS useful in practice. type String_Acc is access String; type T is tagged limited record Name: String_Acc; ... end record; -- ... and in another package: type Finalizer(Ref: access T) is new Controlled with null; type Controlled_T is new T with record F: Finalizer(Controlled_T'Access); -- point to container end record; procedure Finalize(F: in out Finalizer) is procedure Free is new Unchecked_Deallocation(...); begin Free(F.Ref.Name); end Finalize; Now, we can be sure that when objects of type Controlled_T go out of scope, the Name field is deallocated. However, the above example seem implausible. Why didn't type T have finalization in the first place? And if it didn't, why wouldn't it be sufficient to make it a component of the new type, rather than deriving the new type from it? When adding finalization on type extension, the real reason is usually because a component of the extension itself needs finalization. If it's that simple, no access discriminants are necessary -- just add a component of a controlled type, and the component will get finalized at the right time. But what if the component's finalization needs to refer to some other component also added in the extension? In that case, you can put BOTH of those components in a controlled type, and finalize both together. Again, no access discriminants are needed. But what if the component's finalization needs to refer to some other component, and that other component is already in the parent? Such situations seem unlikely. Thus, we find the need to use the multiple inheritance technique to add finalization to be unlikely in practice. Nonetheless, it does work if desired. And the multiple inheritance technique is definitely useful in other situations, involving "mix-ins", as illustrated by LSN-1033. SUMMARY: There are many examples where an object needs to contain a reference to another object, where the referenced object is in the same scope, or in some containing-but-still-nested scope. Access discriminants provide a mechanism for doing so that is safe from dangling pointers. The feature also allows initialization of limited objects at the point of their declaration. And finally, access discriminants are used in the multiple inheritance mechanism of LSN-1033.