Rationale for Ada 2005: Access types

RUSTOP
BACKNEXT

ENG

3. Anonymous access types

@ As just mentioned, Ada 95 permits anonymous access types only as access parameters and access discriminants. And in the latter case only for limited types. Ada 2005 sweeps away these restrictions and permits anonymous access types quite freely.

@ The main motivation for this change concerns type conversion. It often happens that we have a type T somewhere in a program and later discover that we need an access type referring to T in some other part of the program. So we introduce

  1        type Ref_T is access all T;

@ And then we find that we also need a similar access type somewhere else and so declare another access type

  1        type T_Ptr is access all T;

@ If the uses of these two access types overlap then we will find that we have explicit type conversions all over the place despite the fact that they are really the same type. Of course one might argue that planning ahead would help a lot but, as we know, programs often evolve in an unplanned way.

@ A more important example of the curse of explicit type conversion concerns object oriented programming. Access types feature quite widely in many styles of OO programming. We might have a hierarchy of geometrical object types starting with a root abstract type Object thus

  1        type Object is abstract;
  2        type Circle is new Object with ...
  3        type Polygon is new Object with ...
  4        type Pentagon is new Polygon with ...
  5        type Triangle is new Polygon with ...
  6        type Equilateral_Triangle is new Triangle with ...

@ then we might well find ourselves declaring named access types such as

  1        type Ref_Object is access all Object'Class;
  2        type Ref_Circle is access all Circle;
  3        type Ref_Triangle is access all Triangle'Class;
  4        type Ref_Equ_Triangle is access all Equilateral_Triangle;

@ Conversion between these clearly ought to be permitted in many cases. In some cases it can never go wrong and in others a run time check is required. Thus a conversion between a Ref_Circle and a Ref_Object is always possible because every value of Ref_Circle is also a value of Ref_Object but the reverse is not the case. So we might have

  1        RC : Ref_Circle := A_Circle'Access;
  2        RO : Ref_Object;
  3        ...
  4        RO := Ref_Object (RC); -- explicit conversion, no check
  5        ...
  6        RC := Ref_Circle (RO); -- needs a check

@ However, it is a rule of Ada 95 that type conversions between these named access types have to be explicit and give the type name. This is considered to be a nuisance by many programmers because such conversions are allowed without naming the type in other OO languages. It would not be quite so bad if the explicit conversion were only required in those cases where a run time check was necessary.

@ Moreover, these are trivial (view) conversions since they are all just pointers and no actual change of value takes place anyway; all that has to be done is to check that the value is a legal reference for the target type and in many cases this is clear at compilation. So requiring the type name is very annoying.

@ In fact the only conversions between named tagged types (and named access types) that are allowed implicitly in Ada are conversions to a class wide type when it is initialized or when it is a parameter (which is really the same thing).

@ It would have been nice to have been able to relax the rules in Ada 2005 perhaps by saying that a named conversion is only required when a run time check is required. However, such a change would have caused lots of existing programs to become ambiguous.

@ So, rather than meddle with the conversion rules, it was instead decided to permit the use of anonymous access types in more contexts in Ada 2005. Anonymous access types have the interesting property that they are anonymous and so necessarily do not have a name that could be used in a conversion. Thus we can have

  1        RC : access Circle := A_Circle'Access;
  2        RO : access Object'Class; -- default null
  3        ...
  4        RO := RC; -- implicit conversion, no check

@ On the other hand we cannot write

  1        RC := RO; -- implicit conversion, needs a check

@ because the general rule is that if a check is required then the conversion must be explicit. So typically we will still need to introduce named access types for some conversions.

@ We can of course also use null exclusions with anonymous access types thus

  1        RC : not null access Circle := A_Circle'Access;
  2        RO : not null access Object'Class; -- careful

@ The declaration of RO is unfortunate because no initial value is given and the default of null is not permitted and so it will raise Constraint_Error; a worthy compiler will detect this during compilation and give us a friendly warning.

@ Note that we never never write all with anonymous access types.

@ We can of course also use constant with anonymous access types. Note carefully the difference between the following

  1        ACT : access constant T := T1'Access;
  2        CAT : constant access T := T1'Access;

@ In the first case ACT is a variable and can be used to access different objects T1 and T2 of type T.

@ But it cannot be used to change the value of those objects. In the second case CAT is a constant and can only refer to the object given in its initialization. But we can change the value of the object that CAT refers to. So we have

  1        ACT := T2'Access; -- legal, can assign
  2        ACT.all := T2;    -- illegal, constant view
  3        CAT := T2'Access; -- illegal, cannot assign
  4        CAT.all := T2;    -- legal, variable view

@ At first sight this may seem confusing and consideration was given to disallowing the use of constants such as CAT (but permitting ACT which is probably more useful since it protects the accessed value). But the lack of orthogonality was considered very undesirable. Moreover Ada is a left to right language and we are familiar with equivalent constructions such as

  1        type CT is access constant T;
  2        ACT : CT;
  3

@ and

  1        type AT is access T;
  2        CAT : constant AT;

@ (although the alert reader will note that the latter is illegal because I have foolishly used the reserved word at as an identifier).

@ We can of course also write

  1        CACT : constant access constant T := T1'Access;

@ The object CACT is then a constant and provides read-only access to the object T1 it refers to. It cannot be changed to refer to another object such as T2 nor can the value of T1 be changed via CACT.

@ An object of an anonymous access type, like other objects, can also be declared as aliased thus

  1        X : aliased access T;

@ although such constructions are likely to be used rarely.

@ Anonymous access types can also be used as the components of arrays and records. In the Introduction we saw that rather than having to write

  1        type Cell;
  2        type Cell_Ptr is access Cell;
  3        type Cell is
  4                record
  5                        Next  : Cell_Ptr;
  6                        Value : Integer;
  7                end record;

@ we can simply write

  1        type Cell is
  2                record
  3                        Next  : access Cell;
  4                        Value : Integer;
  5                end record;

@ and this not only avoids have to declare the named access type Cell_Ptr but it also avoids the need for the incomplete type declaration of Cell.

@ Permitting this required some changes to a rule regarding the use of a type name within its own declaration – the so-called current instance rule.

@ The original current instance rule was that within a type declaration the type name did not refer to the type itself but to the current object of the type. The following task type declaration illustrates both a legal and illegal use of the task type name within its own declaration. It is essentially an extract from a program in Section 18.10 of [2] which finds prime numbers by a multitasking implementation of the Sieve of Eratosthenes. Each task of the type is associated with a prime number and is responsible for removing multiples of that number and for creating the next task when a new prime number is discovered. It is thus quite natural that the task should need to make a clone of itself.

  1        task type TT (P: Integer) is
  2                ...
  3        end;
  4        type ATT is access TT;
  5        task body TT is
  6                function Make_Clone (N: Integer) return ATT is
  7                begin
  8                        return new TT (N); -- illegal
  9                end Make_Clone;
 10                Ref_Clone : ATT;
 11                ...
 12        begin
 13                ...
 14                Ref_Clone := Make_Clone (N);
 15                ...
 16                abort TT; -- legal
 17                ...
 18        end TT;

@ The attempt to make a slave clone of the task in the function Make_Clone is illegal because within the task type its name refers to the current instance and not to the type. However, the abort statement is permitted and will abort the current instance of the task. In this example the solution is simply to move the function Make_Clone outside the task body.

@ However, this rule would have prevented the use of the type name Cell to declare the component Next within the type Cell and this would have been infuriating since the linked list paradigm is very common.

@ In order to permit this the current instance rule has been changed in Ada 2005 to allow the type name to denote the type itself within an anonymous access type declaration (but not a named access type declaration). So the type Cell is permitted.

@ Note however that in Ada 2005, the task TT still cannot contain the declaration of the function Make_Clone. Although we no longer need to declare the named type ATT since we can now declare Ref_Clone as

  1        Ref_Clone : access TT;

@ and we can declare the function as

  1        function Make_Clone (N : Integer) return access TT is
  2        begin
  3                return new TT (N);
  4        end Make_Clone;

@ where we have an anonymous result type, nevertheless the allocator new TT inside Make_Clone remains illegal if Make_Clone is declared within the task body TT. But such a use is unusual and declaring a distinct external function is hardly a burden.

@ To be honest we can simply declare a subtype of a different name outside the task

  1        subtype XTT is TT;

@ and then we can write new XTT (N); in the function and keep the function hidden inside the task.

@ Indeed we don't need the function anyway because we can just write

  1        Ref_Clone := new XTT (N);

@ in the task body.

@ The introduction of the wider use of anonymous access types requires some revision to the rules concerning type comparisons and conversions. This is achieved by the introduction of a type universal_access by analogy with the types universal_integer and universal_real. Two new equality operators are defined in the package Standard thus

  1        function "=" (Left, Right : universal_access) return Boolean;
  2        function "/=" (Left, Right : universal_access) return Boolean;

@ The literal null is now deemed to be of type universal_access and appropriate conversions are defined as well. These new operations are only applied when at least one of the arguments is of an anonymous access types (not counting null).

@ Interesting problems arise if we define our own equality operation. For example, suppose we wish to do a deep comparison on two lists defined by the type Cell. We might decide to write a recursive function with specification

  1        function "=" (L, R : access Cell) return Boolean;

@ Note that it is easier to use access parameters rather than parameters of type Cell itself because it then caters naturally for cases where null is used to represent an empty list. We might attempt to write the body as

  1        function "=" (L, R : access Cell) return Boolean is
  2        begin
  3                if L = null or R = null then -- wrong =
  4                        return L = R; -- wrong =
  5                elsif L.Value = R.Value then
  6                        return L.Next = R.Next; -- recurses OK
  7                else
  8                        return False;
  9                end if;
 10        end "=" ;

@ But this doesn't work because the calls of "=" in the first two lines recursively call the function being declared whereas we want to call the predefined "=" in these cases.

@ The difficulty is overcome by writing Standard."=" thus

  1        if Standard."=" (L, null) or Standard."=" (R, null) then
  2                return Standard."=" (L, R);

@ The full rules regarding the use of the predefined equality are that it cannot be used if there is a user- defined primitive equality operation for either operand type unless we use the prefix Standard. A similar rule applies to fixed point types as we shall see in a later paper.

@ Another example of the use of the type Cell occurred in the previous paper when we were discussing type extension at nested levels. That example also illustrated that access types have to be named in some circumstances such as when they provide the full type for a private type. We had

  1        package Lists is
  2                type List is limited private; -- private type
  3                ...
  4        private
  5                type Cell is
  6                        record
  7                                Next : access Cell; -- anonymous type
  8                                C    : Colour;
  9                        end record;
 10                type List is access Cell; -- full type
 11        end;
 12        package body Lists is
 13                procedure Iterate (IC : in Iterator'Class; L : in List) is
 14                        This : access Cell := L; -- anonymous type
 15                begin
 16                        while This /= null loop
 17                                IC.Action (This.C); -- dispatches
 18                                This := This.Next;
 19                        end loop;
 20                end Iterate;
 21        end Lists;

@ In this case we have to name the type List because it is a private type. Nevertheless it is convenient to use an anonymous access type to avoid an incomplete declaration of Cell.

@ In the procedure Iterate the local variable This is also of an anonymous type. It is interesting to observe that if This had been declared to be of the named type List then we would have needed an explicit conversion in

  1        This := List (This.Next); -- explicit conversion

@ Remember that we always need an explicit conversion when converting to a named access type.

@ There is clearly an art in using anonymous types to best advantage.

@ The Introduction showed a number of other uses of anonymous access types in arrays and records and as function results when discussing Noah's Ark and other animal situations. We will now turn to more weighty matters.

@ An important matter in the case of access types is accessibility. The accessibility rules are designed to prevent dangling references. The basic rule is that we cannot create an access value if the object referred to has a lesser lifetime than the access type.

@ However there are circumstances where the rule is unnecessarily severe and that was one reason for the introduction of access parameters. Perhaps some recapitulation of the problems would be helpful. Consider

  1        type T is ...
  2        Global : T;
  3        type Ref_T is access all T;
  4        Dodgy : Ref_T;
  5        procedure P (Ptr : access T) is
  6        begin
  7                ...
  8                Dodgy := Ref_T (Ptr); -- dynamic check
  9        end P;
 10
 11        procedure Q (Ptr: Ref_T) is
 12        begin
 13                ...
 14                Dodgy := Ptr; -- legal
 15        end Q;
 16        ...
 17        declare
 18                X : aliased T;
 19        begin
 20                P (X'Access); -- legal
 21                Q (X'Access); -- illegal
 22        end;

@ Here we have an object X with a short lifetime and we must not squirrel away an access referring to X in an object with a longer lifetime such as Dodgy. Nevertheless we want to manipulate X indirectly using a procedure such as P.

@ If the parameter were of a named type such as Ref_T as in the case of the procedure Q then the call would be illegal since within Q we could then assign to a variable such as Dodgy which would then retain the "address" of X after X had ceased to exist.

@ However, the procedure P which uses an access parameter permits the call. The reason is that access parameters carry dynamic accessibility information regarding the actual parameter. This extra information enables checks to be performed only if we attempt to do something foolish within the procedure such as make an assignment to Dodgy. The conversion to the type Ref_T in this assignment fails dynamically and disaster is avoided.

@ But note that if we had called P with P(Global'Access); where Global is declared at the same level as Ref_T then the assignment to Dodgy would be permitted.

@ The accessibility rules for the new uses of anonymous access types are very simple. The accessibility level is simply the level of the enclosing declaration and no dynamic information is involved. (The possibility of preserving dynamic information was considered but this would have led to inefficiencies at the points of use.) In the case of a stand-alone variable such as

  1        V : access Integer;

@ then this is essentially equivalent to

  1        type anon is access all Integer;
  2        V : anon;

@ A similar situation applies in the case of a component of a record or array type. Thus if we have

  1        type R is
  2                record
  3                        C : access Integer;
  4                        ...
  5                end record;

@ then this is essentially equivalent to

  1        type anon is access all Integer;
  2        type R is
  3                record
  4                        C : anon;
  5                        ...
  6                end record;

@ Further if we now declare a derived type then there is no new physical access definition, and the accessibility level is that of the original declaration. Thus consider

  1        procedure Proc is
  2                Local : aliased Integer;
  3                type D is new R;
  4                X : D := D'(C => Local'Access, ... ); -- illegal
  5        begin
  6                ...
  7        end Proc;

@ In this example the accessibility level of the component C of the derived type is the same as that of the parent type R and so the aggregate is illegal. This somewhat surprising rule is necessary to prevent some very strange problems which we will not explore in this paper.

@ One consequence of which users should be aware is that if we assign the value in an access parameter to a local variable of an anonymous access type then the dynamic accessibility of the actual parameter will not be held in the local variable. Thus consider again the example of the procedure P containing the assignment to Dodgy

  1        procedure P (Ptr : access T) is
  2        begin
  3                ...
  4                Dodgy := Ref_T (Ptr); -- dynamic check
  5        end P;

@ and this variation in which we have introduced a local variable of an anonymous access type

  1        procedure P1 (Ptr : access T) is
  2                Local_Ptr : access T;
  3        begin
  4                ...
  5                Local_Ptr := Ptr; -- implicit conversion
  6                Dodgy := Ref_T (Local_Ptr); -- static check, illegal
  7        end P1;

@ Here we have copied the value in the parameter to a local variable before attempting the assignment to Dodgy. (Actually it won't compile but let us analyze it in detail anyway.) The conversion in P using the access parameter Ptr is dynamic and will only fail if the actual parameter has an accessibility level greater than that of the type Ref_T. So it will fail if the actual parameter is X and so raise Program_Error but will pass if it has the same level as the type Ref_T such as the variable Global.

@ In the case of P1, the assignment from Ptr to Local_Ptr involves an implicit conversion and static check which always passes. (Remember that implicit conversions are never allowed if they involve a dynamic check.) However, the conversion in the assignment to Dodgy in P1 is also static and will always fail no matter whether X or Global is passed as actual parameter.

@ So the effective behaviours of P and P1 are the same if the actual parameter is X (they both fail, although one dynamically and the other statically) but will be different if the actual parameter has the same level as the type Ref_T such as the variable Global. The assignment to Dodgy in P will work in the case of Global but the assignment to Dodgy in P1 never works.

@ This is perhaps surprising, an apparently innocuous intermediate assignment has a significant effect because of the implicit conversion and the consequent loss of the accessibility information. In practice this is very unlikely to be a problem. In any event programmers are aware that access parameters are special and carry dynamic information.

@ In this particular example the loss of the accessibility information through the use of the intermediate stand-alone variable is detected at compile time. More elaborate examples can be constructed whereby the problem only shows up at execution time. Thus suppose we introduce a third procedure Agent and modify P and P1 so that we have

  1        procedure Agent (A : access T) is
  2        begin
  3                Dodgy := Ref_T (A); -- dynamic check
  4        end Agent;
  5        procedure P (Ptr : access T) is
  6        begin
  7                Agent (Ptr); -- may be OK
  8        end P;
  9        procedure P1 (Ptr : access T) is
 10                Local_Ptr : access T;
 11        begin
 12                Local_Ptr := Ptr;  -- implicit conversion
 13                Agent (Local_Ptr); -- never OK
 14        end P1;

@ Now we find that P works much as before. The accessibility level passed into P is passed to Agent which then carries out the assignment to Dodgy. If the parameter passed to P is the local X then Program_Error is raised in Agent and propagated to P. If the parameter passed is Global then all is well.

@ The procedure P1 now compiles whereas it did not before. However, because the accessibility of the original parameter is lost by the assignment to Local_Ptr, it is the accessibility level of Local_Ptr that is passed to Agent and this means that the assignment to Dodgy always fails and raises Program_Error irrespective of whether P1 was called with X or Global.

@ If we just want to use another name for some reason then we can avoid the loss of the accessibility level by using renaming. Thus we could have

  1        procedure P2 (Ptr : access T) is
  2                Local_Ptr : access T renames Ptr;
  3        begin
  4                ...
  5                Dodgy := Ref_T (Local_Ptr);  -- dynamic check
  6        end P2;

@ and this will behave exactly as the original procedure P.

@ As usual a renaming just provides another view of the same entity and thus preserves the accessibility information.

@ A renaming can also include not null thus

  1        Local_Ptr : not null access T renames Ptr;

@ Remember that not null must never lie so this is only legal if Ptr is indeed of a type that excludes null (which it will be if Ptr is a controlling access parameter of the procedure P2).

@ A renaming might be useful when the accessed type T has components that we wish to refer to many times in the procedure. For example the accessed type might be the type Cell declared earlier in which case we might usefully have

  1        Next : access Cell renames Ptr.Next;

@ and this will preserve the accessibility information.

@ Anonymous access types can also be used as the result of a function. In the Introduction we had function Mate_Of(A: access Animal'Class) return access Animal'Class; The accessibility level of the result in this case is the same as that of the declaration of the function itself.

@ We can also dispatch on the result of a function if the result is an access to a tagged type. Consider

  1        function Unit return access T;

@ We can suppose that T is a tagged type representing some category of objects such as our geometrical objects and that Unit is a function returning a unit object such as a circle of unit radius or a triangle with unit side.

@ We might also have a function

  1        function Is_Bigger (X, Y : access T) return Boolean;

@ and then

  1        Thing : access T'Class := ... ;
  2        ...
  3        Test : Boolean := Is_Bigger (Thing, Unit);

@ This will dispatch to the function Unit according to the tag of Thing and then of course dispatch to the appropriate function Is_Bigger.

@ The function Unit could also be used as a default value for a parameter thus

  1        function Is_Bigger (X : access T; Y : access T := Unit) return Boolean;

@ Remember that a default used in such a construction has to be tag indeterminate.

@ Permitting anonymous access types as result types eliminates the need to define the concept of a "return by reference" type. This was a strange concept in Ada 95 and primarily concerned limited types (including task and protected types) which of course could not be copied. Enabling us to write access explicitly and thereby tell the truth removes much confusion. Limited types will be discussed in detail in a later paper.

@ Access return types can be a convenient way of getting a constant view of an object such as a table.

@ We might have an array in a package body (or private part) and a function in the specification thus

  1        package P is
  2                type Vector is array (Integer range <>) of Float;
  3                function Read_Vec return access constant Vector;
  4                ...
  5        private
  6
  7        end;
  8        package body P is
  9                The_Vector : aliased Vector :=   ;
 10                function Read_Vec return access constant Vector is
 11                begin
 12                        return The_Vector'Access;
 13                end;
 14                ...
 15        end P;

@ We can now write

  1        X := Read_Vec (7); -- read element of array

@ This is strictly short for

  1        X := Read_Vec.all (7);

@ Note that we cannot write

  1        Read_Vec (7) := Y; -- illegal

@ although we could do so if we removed constant from the return type (in which case we should use a different name for the function).

@ The last new use of anonymous access types concerns discriminants. Remember that a discriminant can be of a named access type or an anonymous access type (as well as oher things). Discriminants of an anonymous access type are known as access discriminants. In Ada 95, access discriminants are only allowed with limited types. Discriminants of a named access type are just additional components with no special properties. But access discriminants of limited types are special. Since the type is limited, the object cannot be changed by a whole record assignment and so the discriminant cannot be changed even if it has defaults. Thus type Minor is ...

  1        type Major (M : access Minor) is limited
  2                record
  3                        ...
  4                end record;
  5        Small : aliased Minor;
  6        Large : Major (Small'Access);

@ The objects Small and Large are now bound permanently together.

@ In Ada 2005, access discriminants are also allowed for nonlimited types. However, defaults are not permitted so that the discriminant cannot be changed so again the objects are bound permanently together. An interesting case arises when the discriminant is provided by an allocator thus

  1        Larger : Major (new Minor ( ... ));

@ In this case we say that the allocated object is a coextension of Larger. Coextensions have the same lifetime as the major object and so are finalized when it is finalized. There are various accessibility and other rules concerning objects which have coextensions which prevent difficulty when returning such objects from functions.

Rationale for Ada 2005: Access types

@ENGRUSTOPBACKNEXT

3. Анонимные ссылочные типы

@ Как уже было упомянуто, Ада 95 разрешает анонимные ссылочные типы только для ссылочных параметров и ссылочных дискриминантов. И в последнем случае только для ограниченных типов. Ада 2005 снимает эти ограничения и разрешает анонимные ссылочные типы весьма свободно.

@ Основным мотивом для этого изменения послужили проблемы с преобразованием типов. Часто случается, что в некоторм месте программы имеется тип T и затем в другой части программы возникает потребность обратиться по ссылке к переменной этого типа. Для этого мы вводим:

  1        type Ref_T is access all T;

@ Затем может возникнуть необходимость в подобном ссылочном типе в другом месте программы. И мы объявляем другой ссылочный тип:

  1        type T_Ptr is access all T;

@ Если использование этих двух ссылочных типов наложится, тогда у нас появится необходимость выполнять явные преобразования из одного типа в доугой несмотря на то, что они действительно одинаковые. Конечно, можно заявить, что предварительное тщательное планирование спасло бы дело, но мы знаем, что программы часто развиваются непредсказуемым образом.

@ Более важный пример вынужденных действий явного преобразования типов касается ООП. Ссылочные типы используются весьма широко во многих аспектах OOП. У нас могла бы быть иерархия геометрических типов, начинающихся с корневого абстрактного типа Object следующим образом:

  1        type Object is abstract;
  2        type Circle is new Object with ...
  3        type Polygon is new Object with ...
  4        type Pentagon is new Polygon with ...
  5        type Triangle is new Polygon with ...
  6        type Equilateral_Triangle is new Triangle with ...

@ тогда мы могли бы объявить именнованные ссылочные типы так:

  1        type Ref_Object is access all Object'Class;
  2        type Ref_Circle is access all Circle;
  3        type Ref_Triangle is access all Triangle'Class;
  4        type Ref_Equ_Triangle is access all Equilateral_Triangle;

@ Понятно, что преобразование между ними должно быть разрешено во многих случаях. В некоторых случаях оно работает правильно, в других - требуется проверка во время выполнения. Таким образом, преобразование между Ref_Circle и Ref_Object всегда возможно, потому что каждое значение Ref_Circle - также значение Ref_Object, но обратное невозможно. Таким образом, мы могли бы иметь:

  1        RC : Ref_Circle := A_Circle'Access;
  2        RO : Ref_Object;
  3        ...
  4        RO := Ref_Object (RC); -- explicit conversion, no check
  5        ...
  6        RC := Ref_Circle (RO); -- needs a check

@ Однако, правила ada требуют, чтобы преобразования типов между именованными ссылочными типами были явными и давали имя типа. Это рассматривается многими программистами как обуза потому, что такие преобразования без именования типов разрешены в других языках OOП. Это не было бы настолько плохо, если бы явное преобразование требовалось только в тех случаях где проверка во время выполнения была бы действительно необходима.

@ Кроме того, эти преобразования весьма тривиальны, так как это всего лишь указатели и никакое реальное изменение значения не требуется; должна выполняться только проверка того, что значение - правильная ссылка целевого типа, и в большинстве случаев это уже ясно при трансляции. Такое требование именования типа только действует на нервы.

@ Фактически единственные преобразования между именованными теговыми типами (и именованными ссылочными типами), которые неявно разрешены в Аде, являются преобразования в надклассовый тип при инициализации или когда это - параметр (что практически - то же самое).

@ Было бы хорошо иметь возможность ослабить правила в Аде 2005, говоря, что именованное преобразование требуется только при проверке во время выполнения. Однако, такое изменение заставило бы много уже написанных программ становиться неоднозначными.

@ Так, вместо того, чтобы влезать в конверсионные правила, в Аде 2005 было разрешено использование анонимных ссылочных типов в большем количестве контекстов. У анонимных ссылочных типов есть интересное свойство, что они являются анонимными и стало быть не имеют названия, которое могло использоваться в преобразовании. Таким образом, мы можем написать:

  1        RC : access Circle := A_Circle'Access;
  2        RO : access Object'Class; -- default null
  3        ...
  4        RO := RC; -- implicit conversion, no check

@ Но мы не можем написать:

  1        RC := RO; -- implicit conversion, needs a check

@ потому что общее правило состоит в том, что если требуется проверка тогда, преобразование должно быть явным. Таким образом, мы все еще должны будем ввести именованные ссылочные типы для некоторых преобразований.

@ Мы можем, конечно, использовать нулевые исключения с анонимными ссылочными типами следующим образом:

  1        RC : not null access Circle := A_Circle'Access;
  2        RO : not null access Object'Class; -- careful

@ Объявление RO неудачно, потому что никакое начальное значение не задано, а устанавливаемое по умолчанию значение пустого указателя не разрешено и, таким образом, это вызывает исключение Constraint_Error; компилятор обнаружит это во время трансляции и выдаст предупреждение.

@ Отметим, что мы никогда никогда не пишем все с анонимными ссылочными типами.

@ Мы конечно, можем использовать константу с анонимными ссылочными типами. Обратите внимание на различие между следующим:

  1        ACT : access constant T := T1'Access;
  2        CAT : constant access T := T1'Access;

@ В первом случае ACT - переменная и может использоваться для обращения к различным объектам T1 и T2 типа T. Но она не может использоваться для измения значения этих объектов. Во втором случае CAT - константа и может только ссылаться на объект заданный при инициализации. Но мы можем изменить значение объекта, к которому обращается CAT. Таким образом, мы имеем:

  1        ACT := T2'Access; -- legal, can assign
  2        ACT.all := T2;    -- illegal, constant view
  3        CAT := T2'Access; -- illegal, cannot assign
  4        CAT.all := T2;    -- legal, variable view

@ На первый взгляд это может показаться запутанным, и было бы неплохо вообще запретить использование констант, таких как CAT (но разрешить ACT, которая вероятно более полезна, так как защищает менять значение, к которому обращаются). Но нехватку ортогональности считали весьма нежелательной. Кроме того, в Аде лево-рекурсивная грамматика, и мы знакомы с эквивалентными конструкциями такими как:

  1        type CT is access constant T;
  2        ACT : CT;
  3

@ и

  1        type AT is access T;
  2        CAT : constant AT;

@ (хотя внимательный читатель отметит, что последний вариант незаконен, потому что по-дурацки использовано зарезервированное слово в как идентификатор).

@ Мы конечно, можем написать:

  1        CACT : constant access constant T := T1'Access;

@ Тогда бъект CACT - константа и обеспечивает доступ только для чтения объекта T1 к которому обращается. Она не может быть изменена для обращения к другому объекту, такому как T2, и при этом значение T1 не может быть изменено через CACT.

@ Объект анонимного ссылочного типа, как и другие объекты, может быть объявлен как aliased следующим образом:

  1        X : aliased access T;

@ хотя такие конструкции, вероятно, будут редко использоваться.

@ Анонимные ссылочные типы также могут использоваться как компоненты массивов и записей. Во Введении мы видели, что вместо необходимости писать:

  1        type Cell;
  2        type Cell_Ptr is access Cell;
  3        type Cell is
  4                record
  5                        Next  : Cell_Ptr;
  6                        Value : Integer;
  7                end record;

@ мы можем просто написать:

  1        type Cell is
  2                record
  3                        Next  : access Cell;
  4                        Value : Integer;
  5                end record;

@ и это не только избавляет нас от необходимости объявлять именованный ссылочный тип Cell_Ptr, но также от необходимости в неполном описании типа Cell.

@ Разрешение этого потребовало небольшого изменения к правилу относительно использования именования типа в пределах его собственного объявления - так называемое правило текущего экземпляра класса.

@ Оригинальное текущее правило экземпляра класса состояло в том, что в пределах описания типа имя типа обращалось не к типу непосредственно, а к текущему объекту типа. Следующее описание задачного типа иллюстрирует и легальное и нелегальное использование имени типа задачи в пределах его собственного объявления. Это - по существу фрагмент программы из Раздела 18.10 [2], который находит простые числа многозадачной реализацией Решета Ерастофена. Каждая задача связана с простым числом, она удаляет множители этого числа и создаёт новую задачу, когда находит следующее простое число. Иными словами, задача должна делать клон самой себя.

  1        task type TT (P: Integer) is
  2                ...
  3        end;
  4        type ATT is access TT;
  5        task body TT is
  6                function Make_Clone (N: Integer) return ATT is
  7                begin
  8                        return new TT (N); -- illegal
  9                end Make_Clone;
 10                Ref_Clone : ATT;
 11                ...
 12        begin
 13                ...
 14                Ref_Clone := Make_Clone (N);
 15                ...
 16                abort TT; -- legal
 17                ...
 18        end TT;

@ Попытка сделать подчиненный клон задачи в функции Make_Clone незаконна, потому что в пределах задачного типа его имя обращается к текущему экземпляру класса а не к типу. Однако, оператор abort разрешен для завершения текущего экземпляра класса задачи. И решение состоит в том, чтобы просто разместить функцию Make_Clone вне тела задачи.

@ Однако, это правило предотвратило бы использование именованного типа Cell для объявления компоненты Next в пределах типа Cell, и это весьма неудобно, так как парадигма связанного списка очень распространена.

@ Чтобы разрешить эту проблему в Аде 2005 было изменено текущее правило экземпляра класса, чтобы позволить имени типа обозначать тип непосредственно в пределах анонимного объявления ссылочного типа (но не при объявлении именованного ссылочного типа). Таким образом, тип Cell разрешен.

@ Отметим, что в Аде 2005 задача TT все еще не может содержать объявление функции Make_Clone. Хотя мы больше не должны объявить именованный тип ATT, так как мы можем теперь объявить Ref_Clone так:

  1        Ref_Clone : access TT;

@ и мы можем объявить функцию так:

  1        function Make_Clone (N : Integer) return access TT is
  2        begin
  3                return new TT (N);
  4        end Make_Clone;

@ где у нас есть анонимный тип результата, однако оператор new TT внутри Make_Clone остается незаконным если Make_Clone объявлен в пределах тела задачи TT. Но такое использование не совсем обычно, тогда как объявление отличной внешней функции не составляет никаких проблем.

@ Чтобы быть честным, мы можем просто объявить подтип с другим именем вне задачи:

  1        subtype XTT is TT;

@ и затем мы можем написать new XTT (N) в функции и сохранить функцию скрытой в задаче.

@ Действительно, мы не нуждаемся в функции так или иначе, потому что мы можем только написать:

  1        Ref_Clone := new XTT (N);

@ в теле задачи.

@ Введение более широкого использования анонимных ссылочных типов требует некоторого пересмотра правил сравнений типов и их преобразований. Это достигнуто введением типа universal_access по аналогии с типами universal_integer и universal_real. Два новых оператора равенства определены в пакете Standard следующим образом:

  1        function "=" (Left, Right : universal_access) return Boolean;
  2        function "/=" (Left, Right : universal_access) return Boolean;

@ Литерал null, как теперь считают, имеет тип universal_access, и соответствующие преобразования определены также. Эти новые операции применимы только, когда по крайней мере один из параметров имеет анонимный ссылочныё тип (не считая null).

@ Интересные проблемы возникают, если мы определяем свою собственную операцию равенства. Например, предположим, что мы желаем сделать глубокое сравнение в двух списков, определенных типом Cell. Мы могли бы написать рекурсивную функцию со спецификацией:

  1        function "=" (L, R : access Cell) return Boolean;

@ Отметим, что ссылочные параметры более легки для использования чем параметры типа Cell непосредственно, потому что обеспечивают более естественное обслуживание случаев когда указатель null используется для обозначения пустого списка. Мы могли бы попытаться написать тело так:

  1        function "=" (L, R : access Cell) return Boolean is
  2        begin
  3                if L = null or R = null then -- wrong =
  4                        return L = R; -- wrong =
  5                elsif L.Value = R.Value then
  6                        return L.Next = R.Next; -- recurses OK
  7                else
  8                        return False;
  9                end if;
 10        end "=" ;

@ Но это не работает, потому что вызовы "=" в первых двух строках рекурсивно вызывают объявляемую функцию, тогда как мы хотим вызвать предопределённую функцию "=" в этих случаях.

@ Трудность преодолевается если написать Standard."=" следующимобразом:

  1        if Standard."=" (L, null) or Standard."=" (R, null) then
  2                return Standard."=" (L, R);

@ Общее правило относительно использования предопределенного равенства состоит в том, что оно не может использоваться, если есть определённая пользователем примитивная операция равенства для любого типа операнда, если мы не используем префикс Standard. Подобное правило относится к fixed point типам, что мы увидем в более поздней статье.

@ Другой пример использования типа Cell находится в предыдущей статье, когда мы обсуждали расширение типа на вложенных уровнях. Тот пример также иллюстрировал что ссылочные типы нужно называть при обстоятельствах, когда они обеспечивают полный тип для частного типа. Мы имели:

  1        package Lists is
  2                type List is limited private; -- private type
  3                ...
  4        private
  5                type Cell is
  6                        record
  7                                Next : access Cell; -- anonymous type
  8                                C    : Colour;
  9                        end record;
 10                type List is access Cell; -- full type
 11        end;
 12        package body Lists is
 13                procedure Iterate (IC : in Iterator'Class; L : in List) is
 14                        This : access Cell := L; -- anonymous type
 15                begin
 16                        while This /= null loop
 17                                IC.Action (This.C); -- dispatches
 18                                This := This.Next;
 19                        end loop;
 20                end Iterate;
 21        end Lists;

@ В этом случае мы должны назвать тип List, потому что это - частный тип. Однако, удобно использовать анонимный ссылочный тип, чтобы избежать неполного объявления Cell.

@ В процедуре Iterate локальная переменная This имеет также анонимный тип. Интересно заметить, что, если бы This был объявлен именованным типом List тогда, мы нуждались бы в явном преобразовании в:

  1        This := List (This.Next); -- explicit conversion

@ Напомним, что мы всегда нуждаемся в явном преобразовании, преобразовывая в именованный ссылочный тип.

@ Таким образом, эффективное использование анонимных типов - целое искусство.

@ Во Введении рассматривались различные случаи использования анонимных ссылочных типов в массивах, записях и возвратах функций при обсуждении Ноева Ковчега и других ситуациях с животными. Теперь обратимся к более сложным вещам.

@ Важный вопрос в случае ссылочных типов - видимость. Правила видимости спроектированы так, чтобы предотвращать 'провисание' ссылок. Основное правило здесь состоит в том, что мы не можем создать значение ссылки, если у объекта на который она ссылается меньшая область видимости чем у ссылочного типа.

@ Однако есть обстоятельства, где это правило излишне серьезно, и это было одной из причин для введения ссылочных параметров. Возможно, некоторый обзор проблем был бы полезен. Рассмотрим:

  1        type T is ...
  2        Global : T;
  3        type Ref_T is access all T;
  4        Dodgy : Ref_T;
  5        procedure P (Ptr : access T) is
  6        begin
  7                ...
  8                Dodgy := Ref_T (Ptr); -- dynamic check
  9        end P;
 10
 11        procedure Q (Ptr: Ref_T) is
 12        begin
 13                ...
 14                Dodgy := Ptr; -- legal
 15        end Q;
 16        ...
 17        declare
 18                X : aliased T;
 19        begin
 20                P (X'Access); -- legal
 21                Q (X'Access); -- illegal
 22        end;

@ Здесь у нас есть объект X с короткой жизнью, и мы не должны пользоваться ссылкой на X в объекте с более длинной жизнью, таком как Dodgy. Однако, мы хотим управлять X косвенно, используя процедуру P.

@ Если бы параметр имел именованный тип, такой как Ref_T как в случае процедуры Q, тогда этот вызов был бы незаконен в пределах Q, который мы могли тогда назначить на переменную, такую как Dodgy, которая сохранит "адрес" X после того, как X прекратил существование.

@ Однако, процедура P, которая использует ссылочный параметр, разрешает запрос. Причина в том, что ссылочные параметры несут динамическую информацию видимости относительно фактического параметра. Эта дополнительная информация дает возможность выполнять динамическую проверку, если мы попытаемся сделать что-то глупое в пределах процедуры, например выполнить присваивание на Dodgy. Преобразования в тип Ref_T в этом случае избавляет от сбоя.

@ Но отметим, что если мы имеем вызов P (Global'Access); где Global переменная объявленая на том же самом уровне где и Ref_T, тогда присваивание на Dodgy было бы разрешено.

@ Новые правила видимости для использования анонимных ссылочных типов очень просты. Уровень видимости это просто уровень объявления, и никакая динамическая информация при этом не используется. (Возможность сохранения динамической информации рассматривалась, но было признано неэффективным). Так объявление локальной переменной:

  1        V : access Integer;

@ эквивалентно:

  1        type anon is access all Integer;
  2        V : anon;

@ Подобная ситуация применяется для компонент записи или массива. Таким образом текст:

  1        type R is
  2                record
  3                        C : access Integer;
  4                        ...
  5                end record;

@ эквивалентен:

  1        type anon is access all Integer;
  2        type R is
  3                record
  4                        C : anon;
  5                        ...
  6                end record;

@ Если мы теперь объявим производный тип тогда никакого нового уровня видимости не создаётся:

  1        procedure Proc is
  2                Local : aliased Integer;
  3                type D is new R;
  4                X : D := D'(C => Local'Access, ... ); -- illegal
  5        begin
  6                ...
  7        end Proc;

@ В этом примере уровень видимости компоненты C производного типа является тем же самым как и родительского типа R и, таким образом, агрегат незаконен. Это несколько удивительное правило необходимо, чтобы предотвратить некоторые очень странные проблемы, которые мы не будем исследовать в этой статье.

@ Выжным последствием, которое необходимо знать, является то, что если мы устанавливаем значение в ссылочном параметре для локальной переменной анонимного ссылочного типа то динамическая видимость фактического параметра не будет проведена в локальной переменной. Рассмотрим снова пример процедуры P, содержащей присваивание для Dodgy:

  1        procedure P (Ptr : access T) is
  2        begin
  3                ...
  4                Dodgy := Ref_T (Ptr); -- dynamic check
  5        end P;

@ и вариант, в котором мы ввели локальную переменную анонимного ссылочного типа:

  1        procedure P1 (Ptr : access T) is
  2                Local_Ptr : access T;
  3        begin
  4                ...
  5                Local_Ptr := Ptr; -- implicit conversion
  6                Dodgy := Ref_T (Local_Ptr); -- static check, illegal
  7        end P1;

@ Здесь мы скопировали значение параметра в локальную переменную перед попыткой присваивания Dodgy. (Фактически это не будет компилироваться, но позволяет нам анализировать это явление.) Преобразование в P с использованием ссылочного параметра Ptr является динамическим и потерпит неудачу если фактический параметр имеет уровень видимости больше чем у типа Ref_T. Т.е., если фактический параметр будет X возбудится исключение Program_Error, но всё будет нормально если параметр будет иметь такой же уровень как у типа Ref_T, такой как у переменной Global.

@ В случае P1, копирование из Ptr в Local_Ptr вызывает неявную конверсионную и статическую проверку, которая всегда выполняется. (Помните, что неявные преобразования запрещены, вовлекают ли они динамическую проверку.) Однако, преобразование в присваивании для Dodgy в P1 является также статическим и будет всегда терпеть неудачу независимо от того X или Global передаются как фактический параметр.

@ Таким образом, поведения P и P1 - одинаковое, если фактический параметр - X (они оба терпят неудачу, хотя один динамически и другой статически), но будет отличным, если фактический параметр будет иметь тот же самый уровень как тип Ref_T, типа переменной Global. Присваивание для Dodgy в P будет работать в случае Global, но присваивание для Dodgy в P1 никогда не будет работать.

@ Удивительно, казалось бы безвредное промежуточное назначение имеет существенный эффект из-за неявного преобразования и последующей потери информации видимости. На практике это вряд ли будет проблемой. В любом случае программисты должны знать, что ссылочные параметры имеют свои особенности и несут динамическую информацию.

@ В этом специфическом примере потеря информации видимости за счёт промежуточной локальной переменной обнаруживается во время компиляции. Но могут быть и более сложные случаи, котрые обнаруживается только во время выполнения. Предположим, что мы вводим третью процедуру Agent и изменяем P и P1 так:

  1        procedure Agent (A : access T) is
  2        begin
  3                Dodgy := Ref_T(A); -- dynamic check
  4        end Agent;
  5        procedure P (Ptr : access T) is
  6        begin
  7                Agent (Ptr); -- may be OK
  8        end P;
  9        procedure P1 (Ptr : access T) is
 10                Local_Ptr : access T;
 11        begin
 12                Local_Ptr := Ptr;  -- implicit conversion
 13                Agent (Local_Ptr); -- never OK
 14        end P1;

@ Теперь мы видим, что P работает как прежде. Уровень видимости, который передаётся в P также передаётся в Agent, где выполняется присваивание Dodgy. Если параметр, который передают в P - локальный X тогда возбуждается Program_Error в Agent и далее размножается в P. Если параметр прошел как Global тогда, всё - хорошо.

@ Процедура P1 теперь компилируется, чего не было прежде. Однако, потому что видимость оригинального параметра потеряна назначением Local_Ptr, теперь это уровень видимости Local_Ptr, который передаётся в Agent, и это означает, что присваивание для Dodgy всегда приводит к возбуждению Program_Error независимо от того, вызывалось ли P1 с параметром X или Global.

@ Если мы по некоторым причинам хотим использовать другое имя, тогда мы можем избежать потери уровня видимости при использовании переименовывания следующим образом:

  1        procedure P2 (Ptr : access T) is
  2                Local_Ptr : access T renames Ptr;
  3        begin
  4                ...
  5                Dodgy := Ref_T (Local_Ptr);  -- dynamic check
  6        end P2;

@ и это будет вести себя точно как оригинальная процедура P.

@ Как обычно переименовывание обеспечивает только другое представление того же самого объекта и, таким образом, сохраняет информацию видимости.

@ Переименовывание может также включить не пустой указатель:

  1        Local_Ptr : not null access T renames Ptr;

@ Напомним, что not null никогда не может быть ложным, и таким образом, это законно только если тип Ptr действительно исключает пустой указатель (это будет, если Ptr будет управляющим ссылочным параметром процедуры P2).

@ Переименовывание оправдано, когда тип T имеет компоненты к которым в процедуре обращаются много раз. Например типом, к которому обращаются мог бы быть тип Cell, объявленный ранее. Тогда:

  1        Next : access Cell renames Ptr.Next;

@ и это сохранит информацию о доступности.

@ Анонимные ссылочные типы могут также использоваться как результат функции. Во Введении мы имели функцию Mate_Of (A: access Animal'Class), возвращающую ссылку на Animal'Class; уровень видимости результата в этом случае - тот же самый что и при объявления функции непосредственно.

@ Мы можем также ссылаться на результат функции, если результат - ссылка на теговый тип. Рассмотрим:

  1        function Unit return access T;

@ Мы можем предположить, что T - теговый тип, представляющий некоторую категорию объектов, типа наших геометрических объектов и что Unit является функцией, возвращей объект типа круга или треугольника.

@ Мы могли бы также иметь функцию:

  1        function Is_Bigger (X, Y : access T) return Boolean;

@ и тогда:

  1        Thing : access T'Class := ... ;
  2        ...
  3        Test : Boolean := Is_Bigger (Thing, Unit);

@ Здесь сначала будет вызов функции Unit затем её результат вместе с Thing будет параметром при вызове функции Is_Bigger.

@ Функция Unit также может использоваться как значение по умолчанию для параметра следующим образом:

  1        function Is_Bigger (X : access T; Y : access T := Unit) return Boolean;

@ Напомним, что значение по умолчанию, используемое в такой конструкции должно быть неопределенным тэгом.

@ Разрешение анонимных ссылочных типов как типов результата устраняет потребность определять понятие "возвращающий по ссылке" тип. Это было странным понятием в Аде 95 и, прежде всего, касалось ограниченных типов (включая задачи, и защищённые типы), который конечно не мог быть скопирован. Предоставление нам ссылки для записи непосредственно устраняет большой беспорядок. Ограниченные типы будут подробно обсуждаться в более поздней статье.

@ Ссылочные return-типы могут быть удобным способом получить постоянное представление таких объектов как таблицы.

@ Мы могли бы иметь массив в теле пакета (или в приватной части) и функцию в спецификации таким образом:

  1        package P is
  2                type Vector is array (Integer range <>) of Float;
  3                function Read_Vec return access constant Vector;
  4                ...
  5        private
  6
  7        end;
  8        package body P is
  9                The_Vector : aliased Vector :=   ;
 10                function Read_Vec return access constant Vector is
 11                begin
 12                        return The_Vector'Access;
 13                end;
 14                ...
 15        end P;

@ Мы можем тогда написать:

  1        X := Read_Vec (7); -- read element of array

@ Это короче чем:

  1        X := Read_Vec.all (7);

@ Но мы не можем написать:

  1        Read_Vec (7) := Y; -- illegal

@ хотя мы могли бы так сделать, если бы мы удалили constant из типа возвращения (когда мы должны использовать другое имя для функции).

@ Последнее новое использование анонимных ссылочных типов касается дискриминантов. Напомним, что дискриминант может иметь именованный ссылочный тип или анонимный ссылочный тип (так же как и все другие типы). Дискриминанты анонимного ссылочного типа известны как ссылочные дискриминанты. В Аде 95 ссылочные дискриминанты позволяются только с ограниченными типами. Дискриминанты именованного ссылочного типа - только дополнительные компоненты без специальных свойств. Но ссылочные дискриминанты ограниченных типов являются особенными. Так как тип ограничен, объект не может быть изменен целым рекордным назначением и, таким образом, дискриминант не может быть изменен, даже если он имеет значение по умолчанию. Тогда, тип Minor

  1        type Major (M : access Minor) is limited
  2                record
  3                        ...
  4                end record;
  5        Small : aliased Minor;
  6        Large : Major (Small'Access);

@ Объекты Small и Large теперь связаны постоянно вместе.

@ В Аде 2005 ссылочные дискриминанты также разрешены для неограниченных типов. Однако, значения по умолчанию присваивается так, что дискриминант не может быть изменен снова и объекты связаны постоянно вместе. Интересный случай возникает, когда дискриминант назначается программой распределения таким образом:

  1        Larger : Major (new Minor ( ... ));

@ В этом случае мы говорим, что распределенный объект - "объект общего происхождения" ("земляк") для Large. Объекты общего происхождения имеют ту же самую жизнь что и главный объект и прекращают своё существование вместе с ним. Есть различия в видимости и в других правилах относительно этих объектов, которые предотвращают трудности при возврате таких объектов из функций.

@ ENG RUS

TOP BACK NEXT

2010-10-24 00:26:54

. .