Rationale for Ada 2005: Access types

RUSTOP
BACKNEXT

ENG

2. Null exclusion and constant

@ In Ada 95, anonymous access types and named access types have unnecessarily different properties.

@ Furthermore anonymous access types only occur as access parameters and access discriminants.

@ Anonymous access types in Ada 95 never have null as a value whereas named access types always have null as a value. Suppose we have the following declarations

  1        type T is
  2                record
  3                        Component : Integer;
  4                end record;
  5        type Ref_T is access T;
  6        T_Ptr: Ref_T;

@ Note that T_Ptr by default will have the value null. Now suppose we have a procedure with an access parameter thus

  1        procedure P (A : access T) is
  2                X : Integer;
  3        begin
  4                X := A.Component; -- read a component of A
  5                -- no check for null in 95
  6                ...
  7        end P;

@ In Ada 95 an access parameter such as A can never have the value null and so there is no need to check for null when doing a dereference such as reading the component A.Component. This is assured by always performing a check when P is called. So calling P with an actual parameter whose value is null such as P(T_Ptr) causes Constraint_Error to be raised at the point of call. The idea was that within P we would have more efficient code for dereferencing and dispatching at the cost of just one check when the procedure is called. Such an access parameter we now refer to as being of a subtype that excludes null.

@ Ada 2005 extends this idea of access types that exclude null to named access types as well. Thus we can write

  1        type Ref_NNT is not null access T;

@ In this case an object of the type Ref_NNT cannot have the value null. An immediate consequence is that all such objects should be explicitly initialized – they will otherwise be initialized to null by default and this will raise Constraint_Error.

@ Since the property of excluding null can now be given explicitly for named types, it was decided that for uniformity, anonymous access types should follow the same rule whenever possible. So, if we want an access parameter such as A to exclude null in Ada 2005 then we have to indicate this in the same way

  1        procedure PNN (A : not null access T) is
  2                X : Integer;
  3        begin
  4                X := A.Component; -- read a component of A
  5                -- no check for null in 2005
  6                ...
  7        end PNN;

@ This means of course that the original procedure

  1        procedure P (A : access T) is
  2                X : Integer;
  3        begin
  4                X := A.Component; -- read a component of A
  5                -- check for null in 2005
  6                ...
  7        end P;

@ behaves slightly differently in Ada 2005 since A is no longer of a type that excludes null. There now has to be a check when accessing the component of the record because null is now an allowed value of A. So in Ada 2005, calling P with a null parameter results in Constraint_Error being raised within P only when we attempt to do the dereference, whereas in Ada 95 it is always raised at the point of call.

@ This is of course technically an incompatibility of an unfortunate kind. Here we have a program that is legal in both Ada 95 and Ada 2005 but it behaves differently at execution time in that Constraint_Error is raised at a different place. But of course, in practice if such a program does raise Constraint_Error in this way then it clearly has a bug and so the difference does not really matter.

@ Various alternative approaches were considered in order to eliminate this incompatibility but they all seemed to be ugly and it was felt that it was best to do the proper thing rather than have a permanent wart.

@ However the situation regarding controlling access parameters is somewhat different. Remember that a controlling parameter is a parameter of a tagged type where the operation is primitive – that is declared alongside the tagged type in a package specification (or inherited of course). Thus consider

  1        package PTT is
  2                type TT is tagged
  3                        record
  4                                Component : Integer;
  5                        end record;
  6                procedure Op (X : access TT); -- primitive operation
  7                ...
  8        end PTT;

@ The type TT is tagged and the procedure Op is a primitive operation and so the access parameter X is a controlling parameter.

@ In this case the anonymous access (sub)type still excludes null as in Ada 95 and so null is not permitted as a parameter. The reason is that controlling parameters provide the tag for dispatching and null has no tag value. Remember that all controlling parameters have to have the same tag. We can add not null to the parameter specification if we wish but to require it explicitly for all controlling parameters was considered to be too much of an incompatibility. But in newly written programs, we should be encouraged to write not null explicitly in order to avoid confusion during maintenance.

@ Another rule regarding null exclusion is that a type derived from a type that excludes null also excludes null. Thus given

  1        type Ref_NNT is not null access T;
  2        type Another_Ref_NNT is new Ref_NNT;

@ then Another_Ref_NNT also excludes null. On the other hand if we start with an access type that does not exclude null then a derived type can exclude null or not thus

  1        type Ref_T is access T;
  2        type Another_Ref_T is new Ref_T;
  3        type ANN_Ref_T is new not null Ref_T;

@ then Another_Ref_T does not exclude null but ANN_Ref_T does exclude null.

@ A technical point is that all access types including anonymous access types in Ada 2005 have null as a value whereas in Ada 95 the anonymous access types did not. It is only subtypes in Ada 2005 that do not always have null as a value. Remember that Ref_NNT is actually a first-named subtype.

@ An important advantage of all access types having null as a value is that it makes interfacing to C much easier. If a parameter in C has type *t then the corresponding parameter in Ada can have type access T and if the C routine needs null passed sometimes then all is well – this was a real pain in Ada 95.

@ An explicit null exclusion can also be used in object declarations much like a constraint. Thus we can have

  1        type Ref_Int is access all Integer;
  2        X : not null Ref_Int := Some_Integer'Access;

@ Note that we must initialize X otherwise the default initialization with null will raise Constraint_Error.

@ In some ways null exclusions have much in common with constraints. We should compare the above with

  1        Y : Integer range 1 .. 10;
  2        ...
  3        Y := 0;

@ Again Constraint_Error is raised because the value is not permitted for the subtype of Y. A difference however is that in the case of X the check is Access_Check whereas in the case of Y it is Range_Check.

@ The fact that a null exclusion is not actually classified as a constraint is seen by the syntax for subtype_indication which in Ada 2005 is

  1        subtype_indication ::= [null_exclusion] subtype_mark [constraint]

@ An explicit null exclusion can also be used in subprogram declarations thus

  1        function F (X : not null Ref_Int) return not null Ref_Int;
  2        procedure P (X : in not null Ref_Int);
  3        procedure Q (X : in out not null Ref_Int);

@ But a difference between null exclusions and constraints is that although we can use a null exclusion in a parameter specification we cannot use a constraint in a parameter specification. Thus

  1        procedure P (X : in not null Ref_Int);     -- legal
  2        procedure Q (X : in Integer range 1 .. N); -- illegal

@ But null exclusions are like constraints in that they are both used in defining subtype conformance and static matching.

@ We can also use a null exclusion with access-to-subprogram types including protected subprograms.

  1        type F is access function (X : Float) return Float;
  2        Fn : not null F := Sqrt'Access;

@ and so on.

@ A null exclusion can also be used in object and subprogram renamings. We will consider subprogram renamings here and object renamings in the next section when we discuss anonymous access types. This is an area where there is a significant difference between null exclusions and constraints.

@ Remember that if an entity is renamed then any constraints are unchanged. We might have

  1        procedure P (X : Positive);
  2        ...
  3        procedure Q (Y : Natural) renames P;
  4        ...
  5        Q(0); -- raises Constraint_Error

@ The call of Q raises Constraint_Error because zero is not an allowed value of Positive. The constraint Natural on the renaming is completely ignored (Ada has been like that since time immemorial).

@ We would have preferred that this sort of peculiar behaviour did not extend to null exclusions.

@ However, we already have the problem that a controlling parameter always excludes null even if it does not say so. So the rule adopted generally with null exclusions is that "null exclusions never lie".

@ In other words, if we give a null exclusion then the entity must exclude null; however, if no null exclusion is given then the entity might nevertheless exclude null for other reasons (as in the case of a controlling parameter).

@ So consider

  1        procedure P (X : not null access T);
  2        ...
  3        procedure Q (Y : access T) renames P; -- OK
  4        ...
  5        Q (null); -- raises Constraint_Error

@ The call of Q raises Constraint_Error because the parameter excludes null even though there is no explicit null exclusion in the renaming. On the other hand (we assume that X is not a controlling parameter)

  1        procedure P (X : access T);
  2        ...
  3        procedure Q (Y : not null access T) renames P; -- NO

@ is illegal because the null exclusion in the renaming is a lie.

@ However, if P had been a primitive operation of T so that X was a controlling parameter then the renaming with the null exclusion would be permitted.

@ Care needs to be taken when a renaming itself is used as a primitive operation. Consider

  1        package P is
  2                type T is tagged ...
  3                procedure One (X : access T); -- excludes null
  4                package Inner is
  5                        procedure Deux (X : access T); -- includes null
  6                        procedure Trois (X : not null access T); -- excludes null
  7                end Inner;
  8                use Inner;
  9                procedure Two (X : access T) renames Deux; -- NO
 10                procedure Three (X : access T) renames Trois; -- OK
 11                ...

@ The procedure One is a primitive operation of T and its parameter X is therefore a controlling parameter and so excludes null even though this is not explicitly stated. However, the declaration of Two is illegal. It is trying to be a dispatching operation of T and therefore its controlling parameter X has to exclude null. But Two is a renaming of Deux whose corresponding parameter does not exclude null and so the renaming is illegal. On the other hand the declaration of Three is permitted because the parameter of Trois does exclude null.

@ The other area that needed unification concerned constant. In Ada 95 a named access type can be an access to constant type rather than an access to variable type thus type Ref_CT is access constant T; Remember that this means that we cannot change the value of an object of type T via the access type.

@ Remember also that Ada 95 introduced more general access types whereas in Ada 83 all access types were pool specific and could only access values created by an allocator. An access type in Ada 95 can also refer to any object marked aliased provided that the access type is declared with all thus

  1        type Ref_VT is access all T;
  2        X : aliased T;
  3        R : Ref_VT := X'Access;

@ So in summary, Ada 95 has three kinds of named access types

  1        access T;         -- pool specific only, read & write
  2        access all T      -- general, read & write
  3        access constant T -- general, read only

@ But in Ada 95, the distinction between variable and constant access parameters is not permitted. Ada 2005 rectifies this by permitting constant with access parameters. So we can write

  1        procedure P (X : access constant T); -- legal 2005
  2        procedure P (X : access T);

@ Observe however, that all is not permitted with access parameters. Ordinary objects can be constant or variable thus

  1        C : constant Integer := 99;
  2        V : Integer;

@ and access parameters follow this pattern. It is named access types that are anomalous because of the need to distinguish pool specific types for compatibility with Ada 83 and the subsequent need to introduce all.

@ In summary, Ada 2005 access parameters can take the following four forms

  1        procedure P1 (X : access T);
  2        procedure P2 (X : access constant T);
  3        procedure P3 (X : not null access T);
  4        procedure P4 (X : not null access constant T);

@ Moreover, as mentioned above, controlling parameters always exclude null even if this is not stated and so in that case P1 and P3 are equivalent. Controlling parameters can also be constant in which case P2 and P4 are equivalent.

@ Similar rules apply to access discriminants; thus they can exclude null and/or be access to constant.

Rationale for Ada 2005: Access types

@ENGRUSTOPBACKNEXT

2. Нулевые исключения и константы

@ В Аде 95 анонимные и именованные ссылочные типы имеют излишне разные свойства.

@ Кроме того, анонимные ссылочные типы разрешены только как ссылочные параметры и ссылочные дискриминанты.

@ Анонимным ссылочным типам в Аде 95 не разрешено иметь пустой указатель в качестве значения, тогда как именованным ссылочным типам это разрешается. Предположим, что мы имеем следующее объявление:

  1        type T is
  2                record
  3                        Component : Integer;
  4                end record;
  5        type Ref_T is access T;
  6        T_Ptr: Ref_T;

@ Здесь T_Ptr назначается пустой указатель по умолчанию. Теперь предположим, что мы имеем процедуру с ссылочным параметром:

  1        procedure P (A : access T) is
  2                X : Integer;
  3        begin
  4                X := A.Component; -- read a component of A
  5                -- no check for null in 95
  6                ...
  7        end P;

@ В Аде 95 ссылочный параметр, такой как A никогда не имеет пустой указатель в качестве значения и, таким образом, нет никакой потребности проверять пустой указатель при обращении к A.Component. Проверка обязательно выполняется при вызове процедуры P. При этом, если фактический параметр поцедуры P окажется равным null то возбуждается исключение Constraint_Error. Идея состояла в том, чтобы в пределах P иметь более эффективный код для разыменования и обработки за счет только одной проверки при вызове процедуры. Такой ссылочный параметр мы считаем подтипом, который исключает пустой указатель.

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

  1        type Ref_NNT is not null access T;

@ В этом случае объект типа Ref_NNT не может иметь пустое значение указателя. Поэтому все объекты этого типа должны быть явно проинициализированы - иначе при инициализации пустым указателем по умолчанию возникнет исключение Constraint_Error.

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

  1        procedure PNN (A : not null access T) is
  2                X : Integer;
  3        begin
  4                X := A.Component; -- read a component of A
  5                -- no check for null in 2005
  6                ...
  7        end PNN;

@ Это означает, что первоначальная процедура:

  1        procedure P (A : access T) is
  2                X : Integer;
  3        begin
  4                X := A.Component; -- read a component of A
  5                -- check for null in 2005
  6                ...
  7        end P;

@ на Аде 2005 ведет себя немного по-другому т.к. A больше не имеет тип, который исключает пустой указатель. Теперь должна выполняться проверка каждый раз при обращении к компоненте записи, потому что пустой указатель теперь - запрещённое значение для A. Так в Аде 2005 вызов процедуры P с нулевым параметром приводит к исключению Constraint_Error возникающему в пределах P только когда мы пытаемся сделать разыменовывание, тогда как на Аде 95 оно всегда возбуждается в точке вызова.

@ Это ранее упоминавшаяся несовместимость особого вида при которой программа является синтаксически правильной и на Аде 95 и на Аде 2005, но при выполнении она ведет себя по-разному, т.к. исключение Constraint_Error возбуждается в разных местах. Но на практике возбуждение исключения Constraint_Error означает явную (несовместимую с жизнью) ошибку и различия (в большинстве случаев) не имеют значения.

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

@ Однако, ситуация относительно управляемых ссылочных параметров несколько отличается. Напомним, что управляющий параметр это параметр тэгового типа, а примитивная операция это операция объявленая рядом с теговым типом в спецификации пакета (или унаследованная от предка). Рассмотрим:

  1        package PTT is
  2                type TT is tagged
  3                        record
  4                                Component : Integer;
  5                        end record;
  6                procedure Op (X : access TT); -- primitive operation
  7                ...
  8        end PTT;

@ Здесь тип TT - теговый, а процедура Op - примитивная операция, и следовательно ссылочный параметр X - управляющий.

@ В этом случае анонимный ссылочный тип (подтип) все еще запрещает пустой указатель, поскольку в Аде 95 нулевой указатель запрещается как параметр. Причина этого состоит в том, что управляющие параметры обеспечивают тэг для диспетчеризации, а пустой указатель не имеет никакого значения тэга. Напомним, что каждый управляющий параметр должен иметь свой тэг. Мы можем добавить not null к спецификации параметра, если пожелаем, но требовать это явно для всех управляющих параметров было бы слишком большой несовместимостью. Но в новых программах мы должны поощрять писать not null явно чтобы избежать беспорядка во время обслуживания.

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

  1        type Ref_NNT is not null access T;
  2        type Another_Ref_NNT is new Ref_NNT;

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

  1        type Ref_T is access T;
  2        type Another_Ref_T is new Ref_T;
  3        type ANN_Ref_T is new not null Ref_T;

@ тогда Another_Ref_T не исключает пустой указатель, но ANN_Ref_T действительно исключает пустой указатель.

@ Технический нюанс здесь в том, что все ссылочные типы, включая анонимные в Аде 2005 разрешают пустой указатель как значение, тогда как в Аде 95 - нет. Только подтипы в Аде 2005 не всегда имеют пустой указатель как значение. Напомним, что Ref_NNT - фактически изначально именованный подтип.

@ Важное преимущество всех ссылочных типов, разрешающих пустой указатель в качестве значения состоит в том, что это значительно упрощает интерфейсную связь с языком C. Если параметр в C имеет тип *t, тогда соответствующий параметр на Аде может иметь ссылочный тип T, и если подпрограмма C нуждается в пустом указателе, тогда не возникает никаких проблем. На Аде 95 это было настоящей головной болью.

@ Явное нулевое исключение может также использоваться в объявлениях объектов таких как constraint. Рассмотрим:

  1        type Ref_Int is access all Integer;
  2        X : not null Ref_Int := Some_Integer'Access;

@ Заметим, что мы должны выполнить явную инициализацию X, иначе инициализация по умолчанию пустым указателем вызовет исключение Constraint_Error.

@ До некоторой степени нулевые исключения имеют много общего с ограничениями. В примере:

  1        Y : Integer range 1 .. 10;
  2        ...
  3        Y := 0;

@ возникнет исключение Constraint_Error потому, что 0-е значения не разрешается для подтипа Y. Различие здесь в том, что в случае X выполняется проверка Access_Check, тогда как в случае Y это - Range_Check.

@ Факт, что нулевое исключение фактически не классифицировано как ограничение посредсвом синтаксиса для subtype_indication, который в Аде 2005:

  1        subtype_indication ::= [null_exclusion] subtype_mark [constraint]

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

  1        function F (X : not null Ref_Int) return not null Ref_Int;
  2        procedure P (X : in not null Ref_Int);
  3        procedure Q (X : in out not null Ref_Int);

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

  1        procedure P (X : in not null Ref_Int);     -- legal
  2        procedure Q (X : in Integer range 1 .. N); -- illegal

@ Но нулевые исключения походят на ограничения когда они оба используются в определении соответствия подтипа и статического соответствия.

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

  1        type F is access function (X : Float) return Float;
  2        Fn : not null F := Sqrt'Access;

@ и так далее.

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

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

  1        procedure P (X : Positive);
  2        ...
  3        procedure Q (Y : Natural) renames P;
  4        ...
  5        Q(0); -- raises Constraint_Error

@ Вызов процедуры Q вызовет исключение Constraint_Error, потому что -0- не позволенное значение для типа Positive. Ограничение Natural при переименовывании полностью игнорируется (Ада ведёт себя так с незапамятных времён).

@ Мы предпочли бы, чтобы этот вид специфического поведения не простирался на нулевые исключения.

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

@ Другими словами, если мы назначаем нулевое исключение, тогда, объект должен исключать пустой указатель всегда; однако, если мы не указываем нулевое исключение тогда, объект может исключать пустой указатель по другим причинам (например в случае управляющего параметра).

@ Рассмотрим:
  1        procedure P (X : not null access T);
  2        ...
  3        procedure Q (Y : access T) renames P; -- OK
  4        ...
  5        Q (null); -- raises Constraint_Error

@ Вызов процедуры Q возбуждает исключение Constraint_Error, потому что параметр исключает пустой указатель даже при том, что нет никакого явного нулевого исключения в переименовывании. С другой стороны (мы предполагаем, что X не управляющий параметр),

  1        procedure P (X : access T);
  2        ...
  3        procedure Q (Y : not null access T) renames P; -- NO

@ является незаконным, потому что нулевое исключение при переименовывании не допускается.

@ Однако, если процедура P является примитивной операцией типа T, и при этом X является управляющим параметром тогда, переименовывание с нулевым исключением разрешается.

@ Должны быть предприняты меры, когда само переименовывание используется как примитивная операция. Рассмотрим:

  1        package P is
  2                type T is tagged ...
  3                procedure One (X : access T); -- excludes null
  4                package Inner is
  5                        procedure Deux (X : access T); -- includes null
  6                        procedure Trois (X : not null access T); -- excludes null
  7                end Inner;
  8                use Inner;
  9                procedure Two (X : access T) renames Deux; -- NO
 10                procedure Three (X : access T) renames Trois; -- OK
 11                ...

@ Процедура One - примитивная операция типа T, его параметр X - управляющий и так исключает пустой указатель даже при том, что это явно не заявлено. Однако, объявление Two незаконно т.к. оно пробует быть операцией диспетчеризации T, и поэтому его управляющий параметр X должен исключить пустой указатель. Но Two - переименовывание Deux, соответствующий параметр которого не исключает пустой указатель и, таким образом, переименовывание незаконно. С другой стороны, объявление Three разрешается, потому что параметр Trois действительно исключает пустой указатель.

@ Другая область, которая нуждалась в унификации касается констант. В Аде 95 именованный ссылочный тип может быть ссылкой к постоянному типу, а не ссылкой к типу переменной, таким образом, тип Ref_CT - ссылка на константу T; Напомним, что это означает, что мы не можем изменить значение объекта типа T через ссылку.

@ Напомним, что Ада 95 ввела более общие ссылочные типы, тогда как в Аде 83 все ссылочные типы были определенным пулом и могли только обратиться к значениям, полученным от системной программы распределения памяти. Ссылочный тип в Аде 95 может также ссылаться на любой объект помеченный aliased при условии, что ссылочный тип объявлен с атрибутом all таким образом:

  1        type Ref_VT is access all T;
  2        X : aliased T;
  3        R : Ref_VT := X'Access;

@ Таким образом, Ада 95 имеет три вида именованных ссылочных типа:

  1        access T;         -- pool specific only, read & write
  2        access all T      -- general, read & write
  3        access constant T -- general, read only

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

  1        procedure P (X : access constant T); -- legal 2005
  2        procedure P (X : access T);

@ Заметим однако, что не всем разрешены сылочные параметры. Обычные объекты могут быть константами или переменными:

  1        C : constant Integer := 99;
  2        V : Integer;

@ и ссылочные параметры следуют за этим образцом. Их называют ссылочными типами, которые являются аномальными из-за необходимости отличать pool specific типы для совместимости с Адой 83 и последующей потребностью их вести.

@ В Аде 2005 ссылочные параметры могут принять следующие четыре формы:

  1        procedure P1 (X : access T);
  2        procedure P2 (X : access constant T);
  3        procedure P3 (X : not null access T);
  4        procedure P4 (X : not null access constant T);

@ Кроме того, как упомянуто выше, управляющие параметры всегда исключают пустой указатель, даже если это явно не заявлено (в том случае P1 и P3 являются эквивалентными). Управляющие параметры могут также быть константными, когда P2 и P4 эквивалентны.

@ Подобные правила применяются к ссылочным дискриминантам; таким образом, они могут исключить пустой указатель и/или быть ссылкой к константе.

@ ENG RUS

TOP BACK NEXT

2010-10-24 00:26:54

. .