Check out the new USENIX Web site. next up previous
Next: Implementation for C++ Up: Using Accessory Functions to Previous: Default Arguments


Encapsulation

Existing single-dispatch object-oriented languages link together the following three properties: (1) The class(es) in which a function is defined, (2) The class(es) representation(s) that a function can access, and (3) The class that is used in dynamic dispatch of the function. In many languages, these three properties unified in the concept of ``the'' class of a method. C++ allows slightly more flexibility by allowing a function to access the representations of several classes if it is listed as a friend (or member) in each of them, though it can only be dynamically dispatched based on the (single) class of which it is a member. Even some multiple-dispatch languages unify the concept of access and dispatch: For example, in Cecil ``a multi-method is granted privileged access to all objects of which the multi-method is a part, i.e., of the objects that are the method's argument constraints'' [5, Section 1.5].

The unification of properties (1) and (2) essentially defines data encapsulation, which plays an essential role in reuse of classes. Since direct access to a class's representation is allowed only from those functions included in the class itself, we can rest assured that uses of the class by other functions (including all ``reuse'') will not corrupt any properties guaranteed by the class as it was originally written. Implicit in the idea of data encapsulation is the principle that programmers will not rampantly add operations to a completed class. If new operations are added to a class, and thus granted access to its representation, we can no longer guarantee that the representation cannot be corrupted. To retain this important property, accessory functions do not have access to classes involved in their dispatch (unless they are listed as friends of that class, for some reason).

We have proposed that the property of dynamic dispatch be separated from the property of inclusion in (and access to) a class. While we originally argued that this be done to support reuse, we find it appealing for several other reasons. First, it provides greater orthogonality of language features. Properties (1) and (2) above must remain unified, but dynamic dispatch is now fully independent. Second, we believe that accessory functions strengthen language support for data encapsulation. One tenet of data encapsulation is that each class should be defined with a set of operations that is both adequate and minimal:

There can also be too many operations in a type... In this case, the abstraction may be less comprehensible, and implementation and maintenance are more difficult. The desirability of extra operations must be balanced against the cost of implementing these operations. If the type is adequate, its operations can be augmented by procedures that are outside the type's implementation. [14, Section 4.9.3]

Stroustrup also discusses this principle [17, Section 11.5.2]. Thus, it can be argued that both the interpret and print_rep functions belong outside the A.S.T. classes in our motivating example, as both can be written efficiently in terms of existing operations. However, without accessory functions, these operations must be placed inside the class.

Note that our need for dynamic dispatch for our A.S.T. example is not simply an artifact of the fact that we have not provided a more abstract way of traversing an abstract syntax tree. If we provide either an iterator or a traversal function to apply arbitrary code to each element of the tree, we still find the need to associate certain code with certain kinds of A.S.T. nodes. Dynamic dispatch provides a simple and efficient mechanism for doing so. Only the restriction of dispatch to members of the class keeps us from using it in these cases.

Since accessory functions do not have access to the private data of the data structure to which they are applied, they cannot save state information in this structure. We must, therefore, accumulate any needed information in some other way. In our ``interpret'' example, information is kept as temporary values in the C++ run-time stack; this works because we only need to produce a single final value (the result of the expression). If we need more complex information, such as a value associated with each node in the tree, we can build up an auxiliary data structure (for example, a second tree that contains values that correspond to the nodes in the A.S.T.). If we wish to traverse the data structure and modify it, we must modify the class (by using traditional virtual functions instead of accessory functions, or making the accessory functions into friends of the class). This is consistent with the principle that only operations listed in the class can access the class's private data.


next up previous
Next: Implementation for C++ Up: Using Accessory Functions to Previous: Default Arguments

2000-12-09