rfg@lupine.ncd.com (Ron Guilmette) (01/23/91)
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).