Topic: Smarter pointers - another solution


Author: rfg@lupine.ncd.com (Ron Guilmette)
Date: 23 Jan 91 09:38:51 GMT
Raw View
If you have no interest in the issue of smart pointers (and how their
use could be made safer) then hit `n' now.

What follows is yet another proposal for a way to make the use of
smart pointers safer.  This bears little (if any) relationship to
my prior proposal(s).

This proposal has some notable advantages over others:  It requires
absolutely no syntactic changes to the language and only very minor
semantic changes.  As a matter of fact, it would actually CLARIFY
one aspect of semantics which is currently murky.  Compatability
with existing stocks of C++ code is (for the most part) maintained.

I welcome your comments.

(Note: Impatient readers may wish to skip directly down to the section
 labeled PROPOSAL SUMMARY.)

----------------------------------------------------------------------------
PROBLEM SUMMARY

When using so-called "smart pointers" for some controlled class C, the
programmer has no way to take complete control over the creation and use
of value of type T* and T& (i.e. "dumb pointers" and "dumb references"
to the class C).  This fact makes it difficult to insure that smart
pointers to some class type (T) are used safely and consistantly throughout
the program.

For example, one programmer may implement the type T along with code which
dynamically relocates objects of type T and then makes corresponding
adjustments to the set of all "smart pointers" to objects of type T.
Another example would be if one programmer implemented a type T along
with a separate smart pointer type for T such that when no more smart
pointers point at a given T object, that object would automatically be
destroyed.

In either of these cases, a different programmer (the "client") may
accidently make use of "dumb pointers" and/or "dumb references" to the
type T such that the dumb pointers and dumb references to type T objects
may (at some points in time) become invalid due to operations occuring
on the type T objects themselves.

If such invalid "dumb reference" or "dumb pointer" values are allow to
develop, and if they are subsequently used, run-time errors (which may
be quite difficult to diagnose or to correct) will probably ensue.

PROBLEM ANALYSIS

The fundamental problem is that the programmer cannot take complete control
over the creation and use of values of type T* and T& (due to the current
C++ language rules) unless he is willing to make the type T itself relatively
inaccessable; an alternative which may be unacceptable for other reasons.

In order to give the programmer complete control over the potential safety
problems which may be associated with uses of dumb T* and dumb T& values,
the language could either:

 o make it impossible to use (i.e. operate upon) values of
  type T* or T& outside of certain limited areas of the
  program, or

 o make it impossible to generate valid (non-null) values of
  type T* or T& outside of certain limited areas of the program.

This proposal considers the latter approach to the problem.  Specifically,
this proposal outlines a means for restricting valid non-null values of
type T* or T& to that region of the program which includes the member
functions of the class type T, its friends, and any classes derived
(either directly or indirectly) from the class type T.

There are five mechanisms by which valid non-null values of type T* or T&
(where the type T is a class type) may be generated.  These are:

 1) A value of type T* may be generated by applying the unary
  & operator to a value of type T.

 2) A value of type T* may be implicitly generated (by the
  compiler) for each call to a non-static member function
  of the class type T.  (This refers to the value that the
  compiler supplies for the `this' pointer.)

 3) A value of type T* may be generated via the implicit
  application of the unary & operator to a value of type
  "array of T" (yielding the address of the zeroth element
  of the array).

 4) A value of type T* (or T&) may be generated via an explicit
  or implicit cast from a different pointer type (or reference
  type) value (or an integral type value) to type T* (or T&).

 5) A value of type T& may be generated via the initialization of
  a variable of type T& with an object of type T.

If the programmer wants to restrict the availability of valid non-null
values of type T* and T& to the scope of the members of class type T (as
described above) then he may partially implement such a restriction via
the following two steps:

 o overload the unary operator& for the class type T, and

 o write all public member functions (and operators, including
  operator&) of the class type T (and any member functions
  and operators of classes derived from T which override
  virtual members of T) such that they do not return (or
  otherwise yield) values of type T* or T& to the outside
  world.

If the programmer follows these steps, he will have taken control only of
mechanism 1 (listed above) by which valid non-null values of type T* and/or
T& may be generated (and subsequently used in unrestricted ways).

Currently, the C++ language rules do not provide the programmer with any
means of taking control of mechanisms 2, 3, 4, and 5 (listed above) by which
valid non-null values of type T* and/or T& might be generated outside of the
restricted context(s) mentioned earlier.


PROPOSAL SUMMARY

In order to provide the programmer with complete control over mechanisms 2,
3, 4, and 5 (listed above) it is proposed that all means by which values of
type T* or T& may be produced should be forced (by language rules) to invoke
one or the other of two "special" member functions (these functions being
`operator&' and the special type-conversion operator `operator T&') whenever
these operators are explicitly declared for a given class.

If language rules forced the invocation of either `operator&' or `operator T&'
for every case in which valid non-null values of type T* or T& (respectively)
could be produced, then then programmer could obtain complete control over
the availability of such values (to various parts of the program) simply by
controlling the visibility and accesability of these two special operators
(of type T).


PROPOSAL

The following new C++ language rules are proposed:

 o Given a class type T for which a unary T::operator& is
  explicitly defined, the interpretation of an expression
  of type "array of T" (in all contexts) is that T::operator&
  is invoked (implicitly) for the zeroth element of the array.
  Thus, the type actually yielded by an expression of type
  "array of T" will be whatever type is yielded by T::operator&.

 o Given a class type T for which a unary T::operator& is
  explicitly defined, the interpretation of each type
  conversion (either implicit or explicit) from any other
  type to a value of type T* is that the normal pointer
  conversion (to type T*) takes place and, following this,
  the member function T::operator& is invoked for the object
  pointed to by the converted pointer value (that is so say
  that the converted pointer value is passed into T::operator&
  as the `this' pointer).

 o Given a class type T for which a user-defined type conversion
  operator `T::operator T&' is explicitly defined, the
  interpretation of each type conversion from any other type
  to a value of type T& is that the normal conversion (to type
  T&) takes place and, following this, the member function
  T::operator T& is invoked for the object refered to by the
  converted reference.

 o The above rules have no effect upon the legality of the
  affected type conversions (i.e. such legality remains
  defined by other existing language rules) except that for
  any additional operator applications which are mandated by
  the rules above (i.e. either T::operator& or T::operator T&)
  the operator(s) involved must be both visable and accessable
  at the point of the conversion or else the conversion is
  illegal.

EXAMPLE

Here is a brief example of how the above rules could be used to create a
(controlled) type T and a "smart pointer" type for T.  In this example,
objects of type T are automatically deleted whenever there are no more
outstanding references to them.

 class smart_ptr_to_T;

 class T {
  int datum;
  int ref_count;
  operator T& ();    /* private */
 public:
  T (int data)
   { datum = data; ref_count = 0; }
  ~T ()
   { if (ref_count > 0) panic (); }

  smart_ptr_to_T operator & (); /* unary & */

  friend class smart_ptr_to_T;
 };

 class smart_ptr_to_T {
  T* pointer;
  smart_ptr_to_T (T* arg)  /* private */
   { pointer = arg; pointer->ref_count++; }
 public:
  smart_ptr_to_T (const smart_ptr_to_T &arg)
   { pointer = arg.pointer; pointer->ref_count++; }
  ~smart_ptr_to_T ()
   { if (--pointer->ref_count == 0) delete pointer; }

  friend class T;
 };

 T::operator T& () { return *this; } /* probably never called */

 smart_ptr_to_T T::operator & ()
 {
  smart_ptr_to_T return_val (this);

  return return_val;
 }

 T T_object (99);  /* OK */
 T& T_ref = T_object;  /* error: operator T& is private */
 smart_ptr_to_T p1 = &T_object; /* OK */
 T* p2 = &T_object;  /* error: can't convert value to T* */


In this example, the classes `T' and `smart_pointer_to_T' are friends of one
another.  This simplifies the code and still prevents "outsiders" from knowing
too much about either type.

It is most important to note that `T::operator T&' is a private member of the
type T.  This makes it impossible to generate values of type T& outside of
the scope of members of T, and thus provides the programmer with a way to
insure that invalid references to objects of type T will not be "floating
around" the program.

PROPOSAL DISCUSSION

The most important aspect of this proposal is that its effect is limited
only to class types and only to those class types for which `operator&'
or `operator T&' are explicitly defined.  Such class types certainly
comprise only a small percentage of class types currently defined within
existing programs.  In fact, while it is likely that some existing programs
do contain classes for which `operator&' is explicitly defined, virtually
no existing programs currently contain classes for which an `operator T&'
(where T is the containing class type) is explicitly defined because the
semantics of defining `operator T&' are currently unspecified.

Also, those (rare) classes which explicitly define their own `operator&',
almost always represent types for which associated "smart pointer" types
are also defined.  Certainly, The mere presence of an explicit definition
for `operator&' within a class clearly indicates the programmer's desire
to seize control over the production of pointers to that type.

This proposal simply adds to the amount of control which the programmer may
exercize in those few cases (and only in those few cases) where the programmer
clearly wants to take control.  Thus, it can safely be assumed that the
effects (on existing code) of adopting this proposal will be minimal and
that these effect (if any) will help rather than hurt existing code.

This idea is similar to tax reform.  The basic idea is to close all of
the existing loopholes.  With the current definition of the C++ language
there are a number of ways (described above) whereby values of "dumb
pointer" types and "dumb reference" types may be spontaneously generated
at various points throughout the program.  This proposal would give the
programmer the ability to restrict the generation of these (potentially
unsafe) pointers and references to only certain limited areas of his
program (in particular, to the pointed-at class, its members and friends).
Then, if problems arise with dumb pointers (or dumb references) getting
incorrect values, the programmer need only consider the code in the
pointed-at class, its members, and its friends, in order to find the
source of the invalid values.

Note that this proposal only provides the programmer with the ability to
control the *generation* of (potentially unsafe) dumb pointer and reference
type values.  It would still be the responsibility of the programmer to
insure that any such dumb pointer and/or dumb reference values which are
generated within the allowed contexts do not "leak out" into other areas
of the program.  It should be easy to exercize such control simply by
insuring that nothing in the pointed-at class (or in its friends or in
its derived classes) allows "dumb" (and potentially unsafe) pointer values
or reference values to "leak out" of the restricted area.

POTENTIAL PROBLEM AREAS

At first glance, there appear to be two potential problems with this
proposal.  As noted below, these "apparent" problems are not "actual"
problems.

The first apparent problem has to do with calling new() to allocate an
array of objects of some type T for which T::operator& is explicitly
defined.

Current language rules require that when new() is called to allocate
an array of objects of some class type T, the global operator new is
invoked rather than any class-specific operator T::new().  That would
be fine except for one thing.  The global operator ::new() is defined
to yield a value of type `void*' (see 12.5).  Note however that an
expression of the form:

 new T[n]

is defined to yield a value of type `T*'.  The implication is that for
such expressions, there is always an implicit conversion of the `void*'
value yielded by the global ::new() to a value of type `T*'.  This (quiet)
type conversion is always provided (implicitly) by the compiler in such
contexts.

Fortunately, the proposed new language rules given above cover this case.
The second of the four rules proposed above calls for "all" conversions
(either explicit or implicit) from any other type to a value of type T*
to automatically invoke `operator&' on the converted value (i.e. passing
the converted value into `operator&' as the `this' pointer).  This rule
would (necessarily) apply to the implicit compiler-supplied conversion
of the value returned by the actual body of the (global) new operator
(which must be of type void*) to the type T* (as required by the context
into which the value is returned).

Note that an interesting (and probably desirable) effect of these rules
would be that:

 smart_pointer_to_T p = new T[10];

would be legal (assuming that `operator&' were explicitly defined for the
type T, that it returned a type `smart_pointer_to_T' value, and that it
was both visible and accessible at the point where the above statement
appeared).

A second apparent problem with this proposal is more serious and may be
cause for more concern.

Under normal circumstances, default copy constructors and assignment
operators are defined (either explicitly or implicitly) for most classes.
The usual definitions of these member functions (for a class type T) assume
that they each take one argument of type `T&'.  In calls of these member
functions, the formal agruments (of type `T&') are initialized from the
actual arguments, which are usually of type `T'.

Unfortunately, if the new rules described above are adopted, then calls to
the default copy constructor and to the default assignment operator for
any class T which defines its own `T::operator T&' (and makes it private)
would become problematic outside of the class T itself (and its friends).
In fact, such calls would be illegal.

How then would one construct a T from another T (using the default copy
constructor) if one cannot even generate the T& value which is needed
as the argument to the constructor?  Likewise, how would one assign a
T to another T if one cannot even generate the required T& (from the
right hand operand of the =)?

As it turns out, calls to the default copy constructor or to the default
assignment operator would indeed become impossible (under the proposed
rules) outside of the class itself (and its friends) if `T::operator T&'
were declared as private to T.

This may initially appear to present severe problems, but in fact it is
quite consistant with the idea of limiting the availability of dumb
pointers and dumb references (outside of T).  If a T& could be created
(in an unrestricted way) from a T and then passed into a default copy
constructor, there is at least some chance that a signal would arrive
and would trigger the relocation (or even the destruction) of the
referenced objects while the copy constructor was executing!  That of
course is exactly the sort of thing that we would like to prevent when
we resort to "smart pointers".

The solution in such cases might not be pretty, but it would be completely
effective.  Simply put, if the user needs either default copy constructors
or default assignment operators to be globally available for a type T for
which `T::operator T&' has been declared private, the user would have to
explicitly define his own copy constructor and assignment operator and
these would each have to accept one argument of the "smart pointer" type
which is (uniquely) associated with the type T.  These explicitly defined
copy constructors and assignment operators could then be invoked, although
the invocations would look a bit odd:

 T object1;
 T object2 (&object1);

  ... object1 = &object2;

Thus, this proposal does not creat any insurmountable problems with respect
to default copy constructors and default assignment operators (even if the
alternative approach used when 'T::operator T&' is private might offend our
artistic sensibilities).