Path: utzoo!utgpu!news-server.csri.toronto.edu!rpi!usc!wuarchive!uunet!mcsun!ukc!ox-prg!bill From: bill@robots.oxford.ac.uk (Bill Triggs) Newsgroups: comp.lang.c++ Subject: Proposal: 'virtual' class members for C++ Message-ID: <1745@culhua.prg.ox.ac.uk> Date: 15 May 91 22:37:11 GMT Sender: news@prg.ox.ac.uk Lines: 270 I would like to propose a simple extension of C++ syntax which would allow 'soft' or 'virtual' class members to be simulated by member functions. A first draft of the proposal is given below. I am soliciting discussion, comments and suggestions, and if any member of the ANSI or ISO committees is interested in submitting the proposal to the appropriate working group I would be most grateful to hear from them. I know that similar schemes have been suggested before, but I feel that the scheme proposed is eminently practical and would be a very worthwhile addition to C++ syntax. Bill Triggs Oxford University Computing Laboratory bill@robots.oxford.ac.uk *long* PROPOSAL FOR 'VIRTUAL CLASS MEMBERS' IN C++. BACKGROUND: It is considered good OOP style to use member functions to hide object state, and this is already possible in C++ using explicit member function calls. The current proposal aims to make this syntactically more pleasant by hiding member function calls as data member accesses. The basic idea is to allow foo.f to be used in place of foo.f() and foo.g = to be used in place of foo.g() in most common circumstances, where 'f' and 'g' are respectively no-argument and one-argument member functions. Function name overloading then allows member functions Type Class::f(); Class::f(Type); to simulate read (non-l-value) and write (l-value) access to a 'virtual' or 'notional' class member 'f' of type 'Type', so that foo.f = bar.f; might stand for foo.f(bar.f()); Reasons for wanting to hide member access behind a member function interface include: (i) Provision of 'virtual' members which are evaluated 'on the fly' from other class state each time they are required. These allow the interface to be more cleanly decoupled from the implementation by effectively hiding the actual state representation behind a 'virtual member' interface, in much the same way as ordinary member functions hide method implementation behind a 'virtual function' interface. (ii) Fine control of access to state (eg read-only access to a 'soft member' is provided by declaring only a 'Type Class::f()' function). (iii) Automatic /housekeeping/constraint maintenance/update validation/ (by transparent channeling of member updates through appropriate housekeeping functions). PROPOSAL: Implicit type conversions should be added to C++ sufficient to allow the automatic conversion of (i) references to no-argument member functions to the functions' return values: Type (Class::)() ---> Type foo.a ---> foo.a() (ii) l-value references to one-argument member functions to one-argument function calls: (Class::)(Type)---> (Class::)((Type)) foo.a = ---> foo.a() (See IMPLEMENTATION below for the suggested exact conversion rules.) DISADVANTAGES: (i) The proposed change is essentially syntactic: the desired functionality is already available by writing out the member function calls explicitly, so the gain amounts to "the elision of a few brackets". ADVANTAGES: (i) For a 'textbook OOP' programming style in which all details of state representation (ie, all member variables) are hidden behind member function calls, there can be a great many brackets to elide! Compare: a()->b().c(d()->e().f(g(), h())); with a->b.c = d->e.f(g, h); Personally, although I agree that wrapping class members is a Good Thing, I seldom do it as I find that the resulting brackets make the code almost unreadable - hence this proposal. (ii) The resulting "overloading of member access" should simplify code maintenance because it helps to decouple member access (interface) from member representation (implementation). At present, if the representation of a class member is changed, the source code of every application which uses the member must also be changed: with virtual members this would not be necessary because the altered class could still provide a 'soft' version of the old interface. My guess is that the reliance of old code on explicit member access is an important barrier to the "class-ification" of older applications. (iii) Judicious use of virtual members should lead to clearer code because "little operations" which "just access state" (member access notation) could be separated from "big operations" which "actually do something" (function call notation). At present the functional notation is often used for both categories: explicitly accessing members is considered bad OOP style as it couples the application to the class representation. Moreover, if some of the members must be wrapped it is often thought preferable to wrap all of them for stylistic uniformity: in this case, the provision of virtual members might *reduce* the number of member wrapping functions required for good OOP style - there is no need to wrap members containing directly exportable state if this can be done later should the representation change. Fewer wrappers would lead to smaller header files and faster compilation for everyone! (iv) The proposed changes are pure extensions to C++ syntax, which should not break any existing code. No new keywords or language constructs are required. IMPLEMENTATION: The required language change amounts to the addition of a few new rules for implicit type conversion. As far as I can see, implementation of these changes would be fairly straightforward. In fact it is remarkable that so few changes are required - it need not have been so simple :-) In detail, suppose we have a data type Type and a class Class: class Class { // .... Type f(); Type f(Type new_f); // .... } foo; Then 'foo.f' in an *r-value* (non-l-value) context would be resolved as follows: 1R) If the 'f' is followed by an opening bracket '(' treat as an explicit member function call, as usual. 2R) Search for a data member 'f' and attempt to resolve as an ordinary member access, as usual. 3R) If the fragment occurs in a context where a pointer-to- member-function 'Type (Class::*pmf)(...)' is expected, search for matching member functions 'f' and attempt to resolve as a p-m-f, as usual. 4R) Search for an argumentless member function 'f', and attempt to resolve using the implicit conversion: Type (Class::)() ---> Type foo.f ---> foo.f() Similarly, 'foo.f = ' and similar *l-value* contexts would be resolved as follows: 1L) Search for a member 'f' and attempt to resolve as an ordinary l-value member access 'foo.f = '. 2L) Search for a single-argument member function 'f' and attempt to resolve as 'foo.f()' by the ordinary function argument matching rules. 3L) Search for an argumentless member function 'f' returning an l-value and attempt to resolve as 'foo.f() = ' using the usual implicit-type-conversion rules. Notes: (i) The only new rules are (4R) and (2,3L), the others are current C++ usage (I hope). (ii) The only problematic case is (3R). Here, the p-m-f interpretation is only possible for a fragment of the form '&foo.f' [ARM 8.2.3], but in this case there *is* a potential collision between the p-m-f interpretation '&(foo.f)' and the address-of-return-value interpretation '&(foo.f())'. For example, a() might return a reference whose address could legitimately be taken. Although the p-m-f interpretation is likely to be very rare in practice, it is given precedence as the "natural" (or at least "current") interpretation of '&foo.f', so that an explicit function call '&foo.f()' might be required to specify the address-of-return-value alternative, depending on the context. (iii) In the l-value case (2,3L), a one-argument function is preferred to a no-argument function because it potentially provides greater functionality. (iv) A precedent for the conversion of 'foo.f = ...' to 'foo.f(...)' in (2L) might be C++ initialiser semantics, see [ARM 8.4]. Similarly, the conversion of 'foo.f' to 'foo.f()' in (3L) mirrors the usage for a no-argument constructor (for which the brackets may be elided [ARM 12.1]). (v) There is no suggestion of relaxing the current rule whereby member function names must be distinct from data member names: this *would* probably be difficult to implement. It would also be confusing to the user and probably not very useful in practice. True data members must have different names from the 'virtual members' which access them, and will most likely be private to their class, however 'virtual members' are genuine member functions and can be overloaded as usual. SUMMARY: By adding a few simple implicit-type-conversion rules to C++, the useful functionality of 'virtual' or 'soft' class members can be provided. These aid the decoupling of class interface from class implementation and ease the problems of software maintenance by providing a 'soft interface' mechanism for backward compatibility. END. -- Bill Triggs Oxford University Computing Laboratory 11 Keble Rd Oxford OX1 3QD