In earlier posts, we saw how the so-called "anonymous" access parameters and discriminants provide a safe mechanism for passing around a shorter-lived object to a longer-lived subprogram, by essentially making it limited for the duration. The syntax allows the client to provide an access value of any type, of any accessibility level, whereas the server must renounce all knowledge of what the actual is.
But this opens up an interesting syntax loophole. An allocator (i.e. a new statement) dynamically allocates a chunk of memory from the heap, and returns an access value to that memory. And since Ada is an expression-based language, nothing is stopping us from writing something like this:
procedure P (x : access Integer);
P(x => new Integer'(42));
At first glance, your gut reaction is likely to sound the memory leak alarm. We allocate a new integer off the heap, and pass it into P as an access parameter. Being that it's an access parameter, we can be certain that no copies have been saved off during the execution of P. And since we never assigned it a variable in the calling code, we have no way to reference it after P completes and the argument x disappears forever. The preceding code, if operating as assumed, would always be an incorrect memory leak.
The language designers noted this, too. They realized that "Unchecked" prefixes Unchecked_Allocation only because there is no guaranteed way to know how many references of that allocation have been made, and thus no way to ensure that no references still exist.
But like we just saw in the legal code above, we can guarantee that no references to the allocation exist after the subprogram call. We know, 100% of the time, that it's no longer unchecked to deallocate that integer. In fact, from a programmer standpoint, there is never a situation in which they could even have a reference to even try. That integer is statically known to be totally inaccessible.
And so buried way in the back of the Ada95 reference manual is a tiny little bit of advice most Ada programmers have no idea exists:
13.11-25 - A storage pool for an anonymous access type should be created at the point of an allocator for the type, and be reclaimed when the designated object becomes inaccessible.
Or, simply stated, automatically reclaim the memory for an allocator of an anonymous access type when it goes out of scope.
So in essence (and usually in practice), that "allocation" from above:
P(x => new Integer'(42));
would actually be compiled to something like this behind your back:
declare
temp : aliased Integer := 42;
begin
P(x => temp'Access);
end;
which is, for the record, what you would (and probably should) have written anyway.
The same holds true for an access discriminant as well. For instance, given
type T (x : access Integer) is null record;
o : T (x => new Integer'(42));
the same thing happens (or, at least, is advised to happen; remember that implementation advice is only that). You end up with something akin to this:
declare
temp : aliased Integer := 42;
o : T (x => temp'Access);
end;
And for a long time, these so-called anonymous allocators were nothing more than a little syntax sugar that nobody really knew about, and fewer people used. Furthermore, the keyword new is almost universally associated with a heap allocation, and so using an anonymous allocator just obfuscated the code while providing no real additional functionality.
But that changed in Ada 2005. One of the major improvements to the 2005 revision was the refinement of limited types. Thus far, the restriction of 'copying' limited types also applied to their initialization since, technically speaking, an aggregate or a function return constituted assignment (i.e. a temporary object is constructed, and then "assigned" into the result).
The new "build-in-place" mechanism fixed this, but added many new subtle problems to deal with. Recall that in Ada95, any type with access discriminants was required to be limited, which as we just saw precludes it from being constructed via an aggregate. The only way to return a limited type was to declare a local (uninitialized) value and return that, which in the case of an anonymous allocator would result in a inevitable runtime check failure. For instance, the following is illegal, since limited aggregates are a no-no:
function F return T is
begin
return T'(x => new Integer'(42)); -- Ada95 illegal!
end F;
whereas we can do this:
function F return T is
Result : T (x => new Integer'(42); --legal, but dubious...
begin
return Result;
end F;
but this will be guaranteed to raise a Program_Error, since our anonymous allocator "allocates" our integer as a local stack value to F (a good compiler should offer a warning).
But now with Ada2005's "build in place" mechanism, the first version becomes legal. This begs the question of where exactly the integer should be allocated. Our previous rule is not really appropriate, being that the "point of the allocator" is still inside the subprogram F, which is different than where the return object itself it. The return object is really being "built into" the result from the calling subprogram, so we might be tempted to say the anonymous allocator should be declared in the previous stack frame. But alas, this has complications as well. For example, we are alright if the previous subprogram is simple:
Foo : T := F; --stack frame is okay
but things get tricky if we change it to this:
Foo : T_Ptr := new T'(F); -- stack frame is no good!
Now our 'outer' object (type T) is being "built into" the heap, so if we were to put our Integer on the stack of the calling program, our outer type might outlive out inner type, and we suddenly have a dangling reference!
So to help clarify things, Ada 2005 created a special name for an anonymous allocator of an access discriminant: the coextension (not to be confused with regular tagged type extension). The rules were updated as follows:
The storage pool used for an allocator of an anonymous access type should be determined as follows:
- If the allocator is defining a coextension of an object being created by an outer allocator, then the storage pool used for the outer allocator should also be used for the coextension;
- For other access discriminants and access parameters, the storage pool should be created at the point of the allocator, and be reclaimed when the allocated object becomes inaccessible;
- Otherwise, a default storage pool should be created at the point where the anonymous access type is elaborated; such a storage pool need not support deallocation of individual objects.
So we can see the original definition there as our second criteria, but we also have two new ones. The third relates to the new Ada2005 "stand alone" access types (and will not be discussed, but note there is not any deallocation!), but the first specifically addresses this new situation of nesting an anonymous allocator of an access discriminant within a return statement of a function being used in an aggregate of an allocator (what a mouthful!).
It states that in this unique situation, the 'inner' coextension should be placed in same place as the 'outer' allocation; if you use the stack for the outer one, then the inner one goes on the stack. If you use the heap for the outer, the inner goes on the heap. If you use a custom storage pool for the outer, the inner goes in the same custom storage pool. You get the idea.
But more to the point, regardless of where it's stored, the inner is finalized at the same time as the outer (See 3.10.2~14.2/2). This can actually provide some unique and powerful opportunities if you know how to use it.
As an example, let's suppose we want to create an OOP-style object that sends messages to another computer over a TCP/IP socket. Externally, we give it a Send_Message operation, so the user can simply call it with a string, which gets packaged up and sent over a socket to the other computer. During the initialization, it opens up a socket to use for subsequent operations, and during finalization it closes it.
But if put on our reusability hats on, we quickly see a problem. We saw that we have to create a new socket as part of the initialization, so we might jump to this conclusion:
type Connection is record
Socket : BSD_Socket_Type;
end Connection;
function Make_Connection return Connection is
begin
return Connection'(Socket => Make_BSD_Style_Socket (Port_Number => 1000));
end Make_Connection;
This would work, of course, except there are many more styles of sockets than just the conventional BSD. Perhaps we might want to use Winsock, or GNAT sockets, or any number of other implementations. We could organize these into a Socket'Class hierarchy, but that doesn't help us much since we have to call a specific concrete constructor function with our desired port number.
Normally we solve this conundrum using a 'factory' object. Apart from our Socket'Class hierarchy, we also have a Socket_Factory'Class for each corresponding socket type. We pass in the factory object, use that to produce the classwide version of the socket, and our connection is now general and reusable enough to handle all sockets, past and present, even at runtime. So we try this approach:
type Connection is record
Socket : Socket'Class; -- Oops!
end Connection;
function Make_Connection (SF : Socket_Factory'Class) return Connection is
begin
return Connection'(Socket => SF.Build(Port_Number => 1000));
end Make_Connection;
But right away we find another problem: classwide types are indefinite, and so there is no way to nest them within a record if the compiler doesn't know the size. In other languages (and in previous versions of Ada), the only answer to this problem was to use the heap:
type Connection is record
Socket : Socket_Class_Ptr;
end Connection;
function Make_Connection (SF : Socket_Factory'Class) return Connection is
s : Socket_Class_Ptr := new Socket'Class'(SF.Build(Port_Number 1000));
begin
return Connection'(Socket => s);
end Make_Connection;
And this works well enough, except now we have to address unpleasant lifetime considerations. We can add a deallocation to a finalize routine, but any experienced programmer will tell you this a sure-fire way to produce memory leaks. If an exception is thrown during our constructor function, we have to add complicated spaghetti code to conditionally deallocate it based on when the exception occurs. Moreover, the only way to get rid of a heap allocation is with an Unchecked_Deallocation which is, of course, Unchecked (and often disallowed in safety critical software). Plus, this precludes us from using this software on embedded processors without a heap, or when allocations must come from a custom storage pool.
A marginally better way is to use a so-called "smart pointer" or "auto pointer" that deallocates the object when it goes out of scope, but aside from still unnecessarily forcing the program onto the heap, that's exactly what an anonymous allocator already does for you. For instance, we can rewrite it like so:
type Connection (Socket : access Socket'Class) is null record;
function Make_Connection (SF : Socket_Factory'Class) return Connection is
begin
return Connection'(Socket => new SF.Build(Port_Number => 1000));
end Make_Connection;
Now we have the best of both worlds (or, depending on your opinion, a weird hybrid mix of both worlds). We are still "allocating" our socket, but not necessarily from the heap. Our socket is saved in the same area as our connection object, so it can work equally well on the stack, the heap, or even custom storage pools. No unchecked mechanisms are needed, and so we are guaranteed to be dangling-reference free and memory leak free.
But this isn't the only situation in which we can benefit. We added a discriminant to our Connection object, which has the unfortunate and often maddening side-effect of making the record itself indefinite (despite not being variant in any way). This means we will have the same problem if we try to compose other objects out of this one (this also applies to the de facto standard mechanism of using unknown discriminants for the public view of a private type to force initialization). For instance, this is problematic:
type Database is record
x : Connection; -- No longer definite!
end Database;
But it's coextensions to the rescue again. We can apply the same logic, all the way up:
type Database (x : access Connection) is null record;
function Make_Database return Database is
begin
return Database'(x => new Connection'(Make_Connection(...)));
end Make_Database;
And, like before, we get to use our indefinite types without resorting to the heap and its inevitable memory leaks, dangling references, and unchecked mechanisms.
For what it's worth, this isn't an especially elegant way to achieve the desired effect: anonymous allocators are probably one of the most "expert friendly" constructs in the entire language, and very little documentation addresses them outside of the AI's and a couple references in the rationale. The fact that so few programmers utilize them means that compilers are mostly untested, and there are often bugs that crop up under certain circumstances. Plus the syntax itself (with the new statement) is almost universally associated with the heap, which is undeniably contrary to Ada's readability goals
But for all it syntax woes, the capability cannot be impugned. For better or worse, there are many circumstances where indefinite types (e.g. classwide types and those with unknown discriminants) can force a programmer onto the heap, usually unnecessarily so. Coextensions (and the new aliased parameters, hopefully to be discussed later) provide a safe, checked, and reliable way to deal with these situations while still leaving the decision of storage pool up to the client.
No comments:
Post a Comment