TEAM-ADA Archives

Team Ada: Ada Programming Language Advocacy

TEAM-ADA@LISTSERV.ACM.ORG

Options: Use Forum View

Use Proportional Font
Show HTML Part by Default
Show All Mail Headers

Message: [<< First] [< Prev] [Next >] [Last >>]
Topic: [<< First] [< Prev] [Next >] [Last >>]
Author: [<< First] [< Prev] [Next >] [Last >>]

Print Reply
Subject:
From:
Stanley Allen <[log in to unmask]>
Reply To:
Stanley Allen <[log in to unmask]>
Date:
Wed, 13 May 1998 00:13:17 -0500
Content-Type:
text/plain
Parts/Attachments:
text/plain (331 lines)
Tucker Taft wrote:
>
> You seem to have a single global reference count.  What is the
> purpose of such a reference count?  Normally a reference count
> is associated with a potentially shared part of an object.  For
> every new reference to the shared part, the count is bumped up.
> When a reference goes away, the count is bumped down.
> If the count goes to zero, you can reclaim the storage of the shared
> part.

The single global reference count is to keep
track of "database interface" objects.  There is
a single count because there is one database, in
shared memory.  The shared memory is allocated
via OS calls when the ref count > 0, deallocated
when the ref count = 0.

> This is easily done in Ada 95 by having a potentially shared
> part of an object include a reference count, initialized to one,
> bumped up by Adjust, and decremented by Finalize.  For example:
>
>  [example snipped]

You are describing something very similar to
what I am attempting -- My "shared part" is
simply a global shared memory block for my
database rather than a part of an object
that can be shared.

> > Unfortunately, I cannot determine what the effect
> > is of declaring such constants because when I feed
> > the following code to my two compilers at home (GNAT
> > and Aonix), the results diverge:
>
> The Aonix compiler is doing the right thing.  The problem is
> that your type "Y" is not a controlled type, since its
> parent type is not visibly controlled anywhere within the immediate
> scope of Y.

Ahargh!  (That's a combo of Aha! and Argh! reflecting
both sudden insight and instant self-reproach.)

Thanks for this valuable insight.

(This is the kind of thing that makes me wish
for a truly advanced OOP in Ada book, something
that delves into all aspects of the subject.  I
posted a note to c.l.a. recently asking if such
a book existed.  I haven't read John English's book
or Mike Smith's; I had the impression that these
were more on the introductory side -- anyone
know better?)

>
> It turns out that Y is *not* controlled, even though you might
> think it is.  X is not visibly derived from Controlled, so Y
> does not inherit, and cannot override, the primitive operations
> Initialize/Adjust/Finalize.  See RM95 7.3.1(6).
>
> >     procedure Initialize (Obj : in out Y);
> >     procedure Adjust (Obj : in out Y);
> >     procedure Finalize (Obj : in out Y);
>
> These declarations don't override anything.  They are declaring
> procedures that just happen to be called "Initialize"/"Adjust"/"Finalize."

Got it.

> >
> >     -- the big question: what's happening here?
> >     Null_Y : constant Y := (X with null record);
>
> Even *if* Y had been controlled, neither Y's Adjust nor Y's Finalize
> would be called, per AI-00083.
>
> In any case, X's (default) Initialize should be called, since
> the extension aggregate you used has an "X" part which needs to
> be default initialized.  The RM allows the compiler to build the
> initial value for Null_Y "in place," so there is no need to copy
> and Adjust the aggregate, so all you should get is a single call
> on X's Initalize, passed the view conversion "X(Null_Y)."
>

Ok, so X gets Initialize'd, and the aggregate as a whole
does not.  I can see why this would apply to deferred
constants, but why would it apply to all objects
initialized with aggregates and all aggregates?

>
> Actually the Aonix results are not bogus.

Naturally. ;)  My apologies to the developers of
the Aonix compiler. ;)

>
> If Y had in fact been controlled, either by being declared in
> a child of Xxx where it could "see" that X was controlled, or
> by making Xxx.X visibly controlled, e.g.:
>
>     type X is new Controlled with private;
>
> then the sequence you should see is:
>
>   Initialize called for object of type X  -- def. init of X part of Null_Y
>   Initialize called for object of type Y  -- default init of local variable Y1
>   Finalize called for object of type Y    -- pre-assign fin. for Y1
>   Adjust called for object of type Y      -- post-assign adj. for Y1
>   Finalize called for object of type Y    -- end of scope for var Y1
>   Finalize called for object of type Y    -- end of scope for Null_Y
>
> So the GNAT results are almost right if Y had been controlled (which it isn't),
> but the GNAT results are still missing the call on Initialize for the
> X part of Null_Y.

This is not at all clear.  In this case it seems
you are saying that the X part of Null_Y will not
be finalized automatically, though it was initialized
so, and that a finalization is performed for Null_Y,
though Null_Y was never initialized or adjusted.
This runs counter to my expectations, and I suppose
it will do so for anyone I teach who comes from a
C++ background.  I was making a gratuitous assumption:
Initialize/Adjust is always symmetrical with respect
to Finalize.

> Note that as good coding practice, the Initialize routine for Y should
> call the Initialize routine for X, the Adjust for Y should call the
> Adjust for X, and the Finalize for Y should call the Finalize for X.

I guess this would be a way of making the Finalize
occur on X, though it would seem that for symmetry
purposes, anything that is automatically Initialized
should also be automatically Finalized.  My assumptions
about the symmetry of Init/Adj/Final are thus not
correct.

> > ...
> > It's not clear to me that RM 7.6(11) that the aggregate
> > as a whole does not cause Init/Adj to be called;
>
> AI-00083 requires that Init/Adj *not* be called for an object
> initialized by an aggregate.  In any case, an aggregate by itself never
> involves a call on Initialize or Adjust for the whole aggregate,
> though it may involve calls on Initialize or Adjust for controlled
> *parts* of the aggregate.

Does this mean, then, that

    X := (Controlled with null record);

does not call Adjust for the temporary object created
by the evaluation of the aggregate?  I thought for
sure that was the point of Norm Cohen's discussion.

> If you change the declaration of X to be
>
>    type X is new Controlled with private;
>
> at least you will begin to see something like what you would expect.
>
> However, it is important to realize that Initialize/Adjust is
> not called on aggregates as a whole, nor on objects initialized with
> an aggregate.  The aggregate must have the value as you want
> it to be seen by Finalize.  Typically, this means that the reference
> count should be set to 1 in the heap object pointed to by a controlled object
> created by an aggregate.  E.g.:
>
>      My_Controlled'(ptr => new heap_obj(ref_count => 1, ...), ...)
>

But this is what I was complaining about before: the
activity you are doing here is a hand-coded version
of what is contained in the Init/Adj routines.
Essentially the same thing is being done in two places
and in different ways, duplicating effort and
introducing a maintenence problem.  Furthermore, this
hand-initialization affects only the value contained in
the object, and therefore cannot do anything "extra"
that you would like performed at object-creation time.

It looks like this gets back to my original point:
apparently there is no way to ensure that a user-
defined activity takes place exactly at the point
of object creation in all cases in such a way that
a corresponding Init or Adj is called for each
Finalize.  You are saying that the *value* in the
object is the most significant thing about it -- but
sometimes you want to reason about the creation and
destruction of objects, without regard to their
contents.  This is why I wanted Create/Destroy
added to the package Ada.Finalization: there are
things I want done besides filling in the component
values.

I suppose, however, that this is an unrealistic
request given the problem with elaboration -- the
Initialize and Adjust procedures can never be called
at the point of a full constant declaration matching
a deferred constant in the spec; the same would be
true for Create/Destroy.  The mirage of symmetry has
finally been dissipated.  C'est la vie!

The following is wrong by the current rules, but
I'm not sure why it must be; i.e., why the rule
about aggregates must be what it is.

----------------------------------------------
with Ada.Finalization; use Ada.Finalization;
package Final is

    type Finis is private;

    function Get_Count return Integer;
    function Create return Finis;

private

    type Finis is new Controlled with
        record
            Has_Been_Finalized : Boolean := False;
        end record;

    procedure Initialize (Obj : in out Finis);
    procedure Adjust (Obj : in out Finis);
    procedure Finalize (Obj : in out Finis);

end Final;
----------------------------------------------
with Text_Io; use Text_Io;
with Ada.Integer_Text_Io; use Ada.Integer_Text_Io;
package body Final is

    Obj_Count : Integer := 0;

    procedure Initialize (Obj : in out Finis) is begin
        Obj_Count := Obj_Count + 1;
        Put (':'); Put (Obj_Count, Width => 0); New_Line; end;

    procedure Adjust (Obj : in out Finis) is begin
        Obj_Count := Obj_Count + 1;
        Put ('='); Put (Obj_Count, Width => 0); New_Line; end;

    procedure Finalize (Obj : in out Finis) is begin
        if not Obj.Has_Been_Finalized then
            Obj_Count := Obj_Count - 1;
            Put ('.'); Put (Obj_Count, Width => 0); New_Line;
            Obj.Has_Been_Finalized := True; end if; end;

    function Get_Count return Integer is begin
        return Obj_Count; end;

    function Create return Finis is begin
        return (Controlled with False); end;    -- problem

end Final;
------------------------------------------------------------
with Final;
with Text_Io; use Text_Io;
with Ada.Finalization; use Ada.Finalization;
procedure Finish3 is

begin

    Put_Line ("X");
    declare
        X : Final.Finis;
    begin
        X := Final.Create;
    end;

    Put_Line ("Y");
    declare
        Y : Final.Finis := Final.Create;
    begin
        null;
    end;

    Put_Line ("Z");
    declare
        Z1 : Final.Finis := Final.Create;
        Z2 : Final.Finis;
    begin
        Z2 := Z1;
    end;

    Put_Line ("end");

end Finish3;
---------------------------------------------------

It produces this output for both Aonix and GNAT:

X
:1
.0
=1
.0
.-1
Y
=0
.-1
.-2
Z
=-1
.-2
:-1
.-2
=-1
.-2
.-3
end

Apparently it does this because the aggregate in
the return statement is never Initialized or
Adjusted, but is Finalized.

Nevertheless, armed with the new understanding,
I can solve my problem: make the Controlled
aspect of my type private so that no external
aggregates can be created "improperly"; eliminate
the aggregate in the return statement; declare a
local constant of my type for use as the default
value to return from the Create procedure, and
"manually" ensure that the Obj_Count reflects that
this has been done.

Thanks once again.

Stanley Allen
mailto:[log in to unmask]

ATOM RSS1 RSS2