Thursday, March 14, 2013

Ada access types, part II


In the previous part, we took a look at the accessibility rules that Ada implements for access values, specifically local ones, to prevent the so-called 'dangling reference' problem.  The main point was that, for better or worse, these rules are mostly static and checked at compile-time.  The trade-off we all make for that particular benefit is that the rules are in many cases overly strict.  As mentioned several times, Ada restricts you from creating a pointer to any value that might potentially ever become a dangling reference, even if your code is written in a way where it does not.

As demonstrated earlier, the only way you can create a local access value is to organize your types so that the access type can't exist for longer than the object (i.e. if there is no longer-lived object to copy the value into, then there can be no dangling reference).  By declaring the access value as a shorter-lived type, we 'imprison' it locally to the scope in which it was created.

But this isn't the end of the story.  Imprisoning an access value within a procedure is often of very little value in a large, complex program.  More often is the case that we must pass the pointer out to another subprogram (usually one not under our control), which presents a catch-22.  Consider the following example:

package K

   type Int_Ptr is access all Integer;

   procedure Q (arg : Int_Ptr) is
   begin
      Ada.Text_IO.Put_Line(Integer'Image(arg.all));
   end Q:

   procedure P is
      x : aliased Integer := 42;
      x_ptr1 : Int_Ptr := x'access;  -- accessibility failure
      type Local_Ptr is access all Integer;
      x_ptr2 : Local_Ptr := x'access;  -- accessibility pass...
   begin
      Q(x_ptr2);  -- but type conversion failure
   end P;

end K;

Just like before, the accessibility rules are doing what they are designed to do.  Both our attempts to copy the pointer outside of P are thwarted for the rules we studied before.

Note in this case, however, that there is no dangling reference.  Nothing 'bad' happens inside Q.  There are no attempts to copy the parameter elsewhere, or any other sort of nefarious, dangling-reference creating behavior.  As written, this code is perfectly safe.

But alas, the pessimistic language rules prevent this from ever compiling.  Q is always at a higher scope than x, so we have no recourse apart from relocating Q inside P (which is rarely possible in production code), or perhaps redefining Q to take an Integer instead unnecessarily using a pointer (assume for example's sake that we cannot).

This is a shame, because there is nothing inherently wrong with this code.  The only problem is that the compiler can't "see into" Q to verify nothing sketchy is taking place, and therefore must assume the worst.  But what if a language construct existed to let Q 'promise' the access value would never be copied to a longer lived type during its execution?  The compiler could then allow P to pass x into Q, confident that no dangling references would be created inside.

To this end, alongside the accessibility rules and 'Access attribute, Ada95 introduced the idea of the anonymous access type.  Like the name suggests, instead of declaring a named type to use for the access value, the declaration is simply put 'in line' where the type would normally be.  For instance, we can rewrite Q from above as follows:

procedure Q (arg : access Integer) is  -- Access Parameter
begin
   Ada.Text_IO.Put_Line(Integer'Image(arg.all));
end Q:

Note the 'type' of arg is now of an anonymous nature, known as an access parameter.  Remember, though, an "access parameter" is a special construct with special rules, which is much different than a parameter of a named access type.

"Anonymizing" our parameter has several important consequences on our program.  Most importantly, we (as the programmers of Q) have essentially "stripped" the parameter of its type information.  Within the body of Q, we have no idea what the 'real' type of arg really is, only that it is of some type of access value that points to an Integer.

But remember, assignment in Ada requires that both types match.  By removing all the type information from our parameter, the object is now incompatible with all types!  If we try to copy arg into a global Int_Ptr, it will fail because, after all, we're not sure arg is an Int_Ptr.  If we try to pass arg as a (named) parameter to some other procedure, it will fail because they are not the same type.  In fact, there isn't a single place we can ever copy arg to, except another access parameter (which, of course, can't copy it anywhere except another access parameter, and so on).

In addition, we clearly don't want Q reassigning the parameter, either, since this would have the unpleasant effect of changing the underlying type of the actual named type to something else.  For this reason, access parameters are defined to be constant (in) parameters.

Now we have turned the tables.  Q can now accept any access value (provided it's to an integer), of any type, with any scope, heap and stack values alike.  P, on the other hand, can be confident no dangling references will exist, since Q will never know the actual type, and thus won't be able to copy it anywhere (this even includes even local values, which would obviously be safe, but that's the trade-off we have to make).


So to recap, access parameters are a mechanism that can be used to safely pass shorter-lived access values to longer-lived subprograms, by ensuring the subprogram can't copy it.  Given that, it's almost always preferably to use an access parameter in lieu of a named access type, assuming you don't need to copy the access value, since it allows your subprogram to work with both general and pool-specific access values.  Unfortunately, the aforementioned confusion, fear, uncertainty, and doubt about accessibility has created a environment where developers refuse to use access parameters, and then complain that the accessibility rules inhibit them.  Such is life, i suppose.

Of course, if you a glass-is-half-empty programmer, you might have noticed the example was deliberately convoluted.  After all, even the least experienced Ada programmer would know better than to use an access value of any kind for Q, and simply used an Integer.  One of the joys of working with Ada is that as mentioned in the first post, you don't have to use pointers for everything like in C.

The majority of the time access values are required, it's for complex data structures (linked lists, etc) which embed the access value within an enclosing record.  An access parameter will not help us with that, since the parameter is not actually an access value.  For instance, let's suppose our integer pointer is within a larger record containing some other meaningless data:

package K

  type Int_Ptr is access all Integer;

  type Big_Record is
    record
      e1 : boolean;
      e2 : Some_Enum;
      e3 : Int_Ptr;
    end record;

  procedure Q (arg : Big_Record ) is
  begin
     Ada.Text_IO.Put_Line(Integer'Image(arg.e3.all));
  end Q:

  procedure P is
     x : aliased Integer := 42;
     y : Big_Record := (foo, bar, x'Access); -- failure!
  begin
     Q(y); -- but type conversion failure
  end P;

end K;

So we have the same situation, except that x is enclosed within a bigger record.  Now we are in trouble, since we must have a type to declare our Big_Record, but such a type would always end up being of a higher scope, resisting any attempt at filling e3 with a local pointer.  This is still a shame, since Q continues to do nothing dubious with the pointer.

In keeping with the same philosophy of access parameters, what if there was a way to provide a kind of "anonymous record element"?  That solution worked for parameters, and might seemingly work for this situation too.  All we want to do is make sure that Q does not copy e3 out to somewhere and create a dangling pointer.

Unfortunately, things are much more complex for record types.  We don't just need to prevent the copying of e3, because copying the entire record type itself implies copying each element, including the pointer.  Similarly, it still needs to be constant so that we cannot change the type to something else, yet records can't have constant elements.

Luckily for us, both constructs already exist in Ada for doing both: limited, discriminated records.  Recall that a record type marked as limited cannot be assigned (or reassigned), which would prevent the copying of the entire record (and consequently the access value embedded inside).  And while Ada does not have the concept of 'constant' record elements, a record's discriminants do have this unique property.

And so along with access parameters, Ada95 also included access discriminants.  They are defined similarly to access parameters, except in a limited record declaration:

 type Big_Record (e3 : access Integer) is limited
   record
     e1 : boolean;
     e2 : Some_Enum;
   end record;

It should be noted at this point that qualifies as an 'abuse of notation' since the record is not, in the strict sense, discriminanted.  The discriminants are simply playing double-duty as normal elements that need to be constant.  Perhaps a better name would have been constant access elements, but i suppose it's a little late for could-of's.

In any case, access discriminants provide the same type of restrictions as access parameters, along with the same benefits.  We can now write our example using local access values, like so:

package K

 type Big_Record (e3 : access Integer) is limited
  record
    e1 : boolean;
    e2 : Some_Enum;
  end record;

 procedure Q (arg : Big_Record) is
 begin
    Ada.Text_IO.Put_Line(Integer'Image(arg.e3.all));
 end Q:

 procedure P is
    x : aliased Integer := 42;
    y : Big_Record := (x'Access, foo, bar); -- pass!
    Q(y);
 end P;

end K;

Just like access parameters, access discriminants give us the ability to pass around local access values to longer lived subprograms, while remaining certain no harm (in the form of dangling references) will come to them.  If Q tries to copy the pointer, it will fail because of the same 'anonymous' type failures as access parameters.  If it tries to copy or reassign the whole record, it will fail because of the limitedness. 

But there is one last problem:  Thus far we've assumed that we will be passing values into subprograms, which for access parameters was obviously true.  But an access discriminant is part of a declared object that can be equally as well passed out of a subprogram (i.e. as a return value), instead of going in.  This has obviously dire consequences, since the scope will be going the wrong way.  For instance

function F return Big_Record is
  x : alised Integer := 42;
begin
  return (x'Access, foo, bar);
end F;

is the textbook example of a dangling reference (x ceases to exist when the function returns, yet the return value has a reference to it).

Solving this conundrum requires one of the few runtime checks in the Ada accessibility rules (specifically mentioned in 6.5~21/3).  This code will compile and run, yet raise Program_Error upon executing.  Or, at least, it should: most versions of GNAT (and GNAT-based compilers) completely blow this discriminant check, and such buggy implementations produce code that actually does have dangling references.  Hopefully a supported GNAT Pro user will use their clout to get such problems fixed (they don't seem to listen to me).  Such is life.

So between access parameters and access discriminants, a programmer has the mechanisms to work safely with local access values while being statically (in most cases) guaranteed that pointers won't be left dangling.  The only situations where the preceding methods won't work are those in which you want to copy a pointer, which is, of course, the exact thing the rules are trying to prevent.  If you do need to copy a pointer somewhere longer-lived, you need to use a named access type (in actuality, you need to use a shared pointer, but that's a different post).

Yet all is not well.  Next time, we will see how a simple idea like access parameters got way, way out of hand.

No comments:

Post a Comment