bill@robots.oxford.ac.uk (Bill Triggs) (05/16/91)
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 = <expression>
to be used in place of
foo.g(<expression>)
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:
<l-value>(Class::)(Type)---> (Class::)((Type)<r-value>)
foo.a = <expression> ---> foo.a(<expression>)
(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 = <expression>' 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 = <expression>'.
2L) Search for a single-argument member function 'f' and
attempt to resolve as 'foo.f(<expression>)' 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() =
<expression>' 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
gyro@kestrel.edu (Scott Layson Burson) (05/25/91)
In article <1745@culhua.prg.ox.ac.uk> bill@robots.oxford.ac.uk (Bill Triggs) writes: >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. > >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 = <expression> > >to be used in place of > > foo.g(<expression>) > >[...] I like the basic idea of having things that look like member variables that are actually member function invocations. However, I don't like the automatic conversion from one to the other, primarily because it conflicts with another proposal of which I am very fond (that the meaning of `foo.f', with no parens, should be the closure of `f' over `foo'). How about this instead: we call these "virtual member variables" and declare them something like this: class Foo { int x; int read_x() { return x; } void set_x(int new_x) { x = new_x; } public: virtual int value = { read_x, set_x }; }; [Syntax notes: 1) This represents yet another overloading of `virtual'. I'm not too fond of the way `virtual' is overloaded already, but this usage makes as much sense as either of the others. 2) The declaration syntax for virtual member variables requires no grammar changes, but it does introduce an entirely different meaning for the "initializer", which may be considered untasteful.] Then one could write: Foo foo; foo.value = 3; // invokes `foo.set_x(3)' cout << foo.value; // invokes `foo.read_x()' This requires only one additional declaration compared to the original proposal, and has the virtue (in my eyes) of not being automagic. Flames invited... -- Scott Gyro@Reasoning.COM
struan@fivegl.co.nz (Struan Judd) (05/28/91)
In article <1991May24.170014.17301@kestrel.edu> gyro@kestrel.edu (Scott Layson Burson) writes: >In article <1745@culhua.prg.ox.ac.uk> bill@robots.oxford.ac.uk (Bill Triggs) writes: >>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. >> >>[...] > >I like the basic idea of having things that look like member variables >that are actually member function invocations. However, I don't like > >[...] > >-- Scott >Gyro@Reasoning.COM Throwing my $0.02 into the ring on this general subject, I quite like using the following style. While it does not hide the fact that assignments to variables are being done through a function interface, it does allow non-friends of the class 'access' to some of the member variables of the class. Example: class Scr_Location { private: unsigned char SL_Left, SL_Top, SL_Right, SL_Bottom; public: Scr_Location(void); . . . const unsigned char &Left = SL_Left, &Right = SL_Right, &Top = SL_Top, &Bottom = SL_Bottom; . . . }; (Question: It would be very nice if C++ compilers could recognise this construct and not actually reserve storage in the class instances for the references. Do any do this?) This works for me as I tend to only have code executed when variable values are changed not when they are merely obtained. -- +------------------------------------------------+---------------------+ | Struan L. Judd | struan@fivegl.co.nz | | Of all the bit boxes in this electronic world, | PH: +64 9 302-1621 | | this message had to shift into this one. | FAX: +64 9 302-1617 |
Struan.Judd@sunbrk.FidoNet.Org (Struan Judd) (05/28/91)
Reply-To: struan@fivegl (Struan Judd) In article <1991May24.170014.17301@kestrel.edu> gyro@kestrel.edu (Scott Layson Burson) writes: >In article <1745@culhua.prg.ox.ac.uk> bill@robots.oxford.ac.uk (Bill Triggs) writes: >>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. >> >>[...] > >I like the basic idea of having things that look like member variables >that are actually member function invocations. However, I don't like > >[...] > >-- Scott >Gyro@Reasoning.COM Throwing my $0.02 into the ring on this general subject, I quite like using the following style. While it does not hide the fact that assignments to variables are being done through a function interface, it does allow non-friends of the class 'access' to some of the member variables of the class. Example: class Scr_Location { private: unsigned char SL_Left, SL_Top, SL_Right, SL_Bottom; public: Scr_Location(void); . . . const unsigned char &Left = SL_Left, &Right = SL_Right, &Top = SL_Top, &Bottom = SL_Bottom; . . . }; (Question: It would be very nice if C++ compilers could recognise this construct and not actually reserve storage in the class instances for the references. Do any do this?) This works for me as I tend to only have code executed when variable values are changed not when they are merely obtained. -- +------------------------------------------------+---------------------+ | Struan L. Judd | struan@fivegl.co.nz | | Of all the bit boxes in this electronic world, | PH: +64 9 302-1621 | | this message had to shift into this one. | FAX: +64 9 302-1617 | * Origin: Seaeast - Fidonet<->Usenet Gateway - sunbrk (1:343/15.0)
chip@tct.com (Chip Salzenberg) (06/01/91)
According to Struan.Judd@sunbrk.FidoNet.Org (Struan Judd): > >class Scr_Location { > private: > unsigned char SL_Left, SL_Top, SL_Right, SL_Bottom; > public: > Scr_Location(void); > . > . > . > const unsigned char &Left = SL_Left, &Right = SL_Right, > &Top = SL_Top, &Bottom = SL_Bottom; > . > . > . >}; I don't consider this kind of kludginess and storage waste to be worth the notational "advantage" of "x.a" over "x.a()". -- Chip Salzenberg at Teltronics/TCT <chip@tct.com>, <uunet!pdn!tct!chip> perl -e 'sub do { print "extinct!\n"; } do do()'