[comp.std.c++] ~const Rev 1.6

ngo@tammy.harvard.edu (Tom Ngo) (12/14/90)

$Revision: 1.6 $

I would like to propose an enhancement to C++.  This is in preparation
for submission to the ANSI committee.  The proposal is broken into
four short sections:

    1  Brief statement of the proposal
    2  More details about the proposal
    3  An example in which it would be useful
    4  Some debate over the proposal
    5  A possible extension about which I have reservations

I posted the original form of this proposal on 12/11/90.  In response
to that posting, a member of the ANSI committee suggested that I
extend the proposal to include member functions.  So I canceled the
original posting.  If you still have it, please delete it and use this
version instead.

BRIEF STATEMENT:

    I would propose a new specifier, ~const.  Only members of a class
    can be specified as ~const.  A data member that is specified as
    ~const can be modified even if the object of which it is a part is
    specified as const.  A member function that is specified as ~const
    can modify any data member even if the object for which it is
    called is specified as const.

    The primary purpose of the ~const specifier is to obviate some
    common cases in which the const attribute would otherwise have to
    be cast away.

MORE DETAILS:

1.  Following E&S 7.1.6 [Type Specifiers], a type-specifier can be:
	simple-type-name
	...
	const
	volatile
	~const
    The ~const specifier may appear in the type-specifier of a
    nonstatic, nonpointer member, whether it is data or a function.

2.  Following the same section, each element of a const array is
    const.  Each nonfunction, nonstatic, nonpointer member of a const
    class object is const unless it is specified as ~const.

3.  Following E&S 9.3.1 [The this Pointer]:

    A const member function may be called for const and non-const
    objects.  The type of this in such a member function of a class X
    is "const X *const".

    A non-const member function that is not specified as ~const may be
    called only for a non-const object.  The type of this in such a
    member function is "X *const".

    A member function that is specified as ~const may be called for
    const and non-const objects.  The type of this in either case is
    "X *const".

4.  Following E&S 8 [Declarators], a cv-qualifier can be:
	const
	volatile
	~const
    The ~const specifier may appear in the cv-qualifier of a pointer
    that is a nonstatic member.  And as usual, "The cv-qualifiers
    apply to the pointer and not to the object pointed to."  (E&S
    8.2.1.)

5.  The ~const specifier does not create any new distinct types.  It
    merely removes the const attribute.  Therefore the introduction of
    this specifier should require no new type matching rules.

EXAMPLE:

    The code below is intended to be short but self-contained.
    Consequently, it isn't the best example imaginable, but it
    illustrates the point.

    The class Angle holds a double whose sine and cosine are expected
    to be evaluated--possibly often, possibly not at all.  We
    presuppose the existence of a function sincos() which
    simultaneously computes the sine and cosine of a double in an
    efficient manner.  (Suns have such a function.)  The approach in
    this implementation of Angle is to cache the sine and cosine the
    first time either one is required.

    extern void sincos( const double x, double *const s, double *const c);
    class Angle {
      private:
	double x;
	~const double sinx, cosx;
	~const int cached;
	void cache(void) const
          { if (!cached) { sincos( x, &sinx, &cosx ); cached = 1; } }
      public:
	Angle(const double _x) : x(_x), cached(0), sinx(0), cosx(0) {}
	operator = (const double _x) { x = _x; cached = 0; }
	double sin() const { cache(); return sinx; }
	double cos() const { cache(); return cosx; }
    };

    The structure of which the class A is an abstraction (i.e. x) is
    not modified by the methods sin() and cos().  Therefore, it makes
    sense to declare those two methods const--the cache is an
    implementation detail that the user of Angle should not have to
    think about.

    Without the ~const specifier, one of two things must happen.
    Either cache(), sin() and cos() cannot be const--which, to me is
    unacceptable--or cache() must refer to cached, sinx and cosx
    through this, with const explicitly cast away.  With the ~const
    specifier, the cache() method is much easier to read.  In
    addition, someone reading the class declaration can tell right
    away that cached, sinx and cosx form some kind of cache that is
    transparent to the user.

    An alternative solution is to remove the ~const specifier from the
    data members sinx, cosx and cached, and to declare "void
    cache(void) ~const".  In my opinion this is less expressive since
    the user has to read and understand the code for Angle::cache() to
    find out that sinx, cosx and cached form a cache.

DEBATE:

Here, each idea is marked [+] or [-], depending on whether I think it
supports or detracts from the proposal.

[+] The semantics of ~const are simple:  ~const breaks the propagation
    of the const attribute from an aggregate or object to its
    components.

[-] The existing rule is even simpler: once you declare an aggregate
    or object const, its constness is guaranteed to propagate
    recursively to all of its subcomponents.  Similarly, once you
    declare a pointer const, the constness propagates to all
    subcomponents of its referent.  This proposal would represent the
    only violation of that rule.  [+] On the other hand, the
    applications of ~const are very real, and the only other way to
    implement them at the moment is by casting away const.  It would
    be better to choose the lesser of two evils.

[-] The existence of a ~const specifier might make the compiler's job
    somewhat more complicated.  While determining whether a function
    has side effects, an object that is declared "const" can no longer
    be assumed not to be modified.  [+] On the other hand, the ability
    to cast away const presents the same problem--and again, people
    will do one or the other (use ~const or cast away const).

    One might anticipate the complexity of handling the ~const
    specifier to be similar to the complexity of handling virtual
    functions.  With virtual functions, one must look at all base
    classes to determine whether a vtbl is needed for a class.  With
    ~const members, one must look at base classes and members that are
    objects to determine whether any ~const specifiers are present.

[+] The ~const specifier can remove some problems of the type alluded
    to in various places in E&S, most notably the end of section 5.4
    [Explicit Type Conversion].  The problem is that the effect of
    casting away const is implementation dependent.  At present, a
    compiler is free to place a const object in readonly memory.  With
    ~const, one might add the additional rule that (at least) ~const
    data members cannot be in readonly memory.

[-] In the hands of the wrong programmer, the ~const specifier
    could lead to sloppiness.  [+] On the other hand, all of C++
    relies on the neatness of the programmer.  For instance, a sloppy
    programmer might declare all members public and might never use
    "const" except where constrained to do so by existing libraries.
    This is no reason not to have those specifiers.

[-] Nothing can be done with the ~const specifier that can't
    already be done with the existing constructs.  [+] On the other
    hand, there are other things in C++ that are really just syntactic
    sugar.  For instance, there is really no difference between a
    reference "X&" and a constant pointer "X *const", besides the 
    obvious syntactic difference.

[+] The ~const specifier can become an idiom which expresses something
    that goes on naturally in the mind of a programmer.  I always make
    a distinction between those data members that are part of the
    abstraction represented by a class, and those that are not.  At
    present, the best I can do is use comments to mark the
    distinction.

[+] Adding a ~const specifier would introduce no new keywords, and
    would not break existing code.

POSSIBLE EXTENSION:

    The following item has been suggested to me, and I include it here
    not as part of this proposal, but as food for thought.  The idea
    ought to be explored, if only because it is a natural next step
    from the proposal as I have framed it.

    Caveats: this suggestion's usefulness depends on the existence of
    a language feature not mentioned in the ARM, and it opens a
    Pandora's box of related possibilities.

    I am told that a possible extension to C++ is to have run-time
    type information (e.g. "typeof").  Such a facility is presently
    included in gcc/g++, and the next few paragraphs use the syntax
    implemented therein.  Briefly, the typeof facility in gcc/g++
    permits referring to the type of an expression and can be used any
    place that a type name could normally be used.  Simple example:

        class Foo { ... };
        const Foo x;
        typeof (x) y;           // declares y to be a const Foo

    Now, the suggested extension to this proposal is to permit the use
    of ~const to remove the const attribute from an unknown type, as
    when x happens to be a macro parameter:

        #define nonconst(a) ~const typeof(a)
        nonconst(x) z;     // declares z to be a non-const Foo

    The semantics of ~const in this context are different from the
    semantics in my proposal.  Here, ~const removes the const
    attribute from the type that it modifies.  In my proposal, ~const
    breaks the propagation of the const attribute from an enclosing
    structure.  This difference in semantics leads to ambiguity if
    (referring to the example above) z happens to be a member of some
    class:

        class Bar {
            ~const typeof(x) z;
            ...
        };

    Do we mean for z to have the type of x, except with the const
    attribute removed?  Or do we mean for z to be modifiable even if
    the Bar object of which it is a part happens to be const?

    If this usage of ~const were to be adopted, we would be obligated
    to consider a similar type-specifier, ~volatile, whose purpose is
    to remove the volatile attribute from an unknown type.
--
  Tom Ngo
  ngo@harvard.harvard.edu
  617/495-1768 lab number, leave message

schwartz@groucho.cs.psu.edu (Scott Schwartz) (12/15/90)

ngo@tammy.harvard.edu (Tom Ngo) writes:
       If this usage of ~const were to be adopted, we would be obligated
       to consider a similar type-specifier, ~volatile, ....

And maybe even ~alias, too. :-)

jbuck@galileo.berkeley.edu (Joe Buck) (12/16/90)

ngo@tammy.harvard.edu (Tom Ngo) writes:
>        If this usage of ~const were to be adopted, we would be obligated
>        to consider a similar type-specifier, ~volatile, ....

No, we wouldn't.  ~volatile wouldn't buy you anything.  It would presumably
be used to specify that a specific member in a volatile object is not
volatile.  While that would give a tiny bit of useful information to
an optimizing compiler, it would gain nothing in expressive capability.
I'm not sure I'm for the ~const feature, but the proposer does have an
argument.  I don't see the corresponding argument existing for ~volatile.
You might want to include it anyway for consistency in the grammar.

schwartz@groucho.cs.psu.edu (Scott Schwartz) writes:
> And maybe even ~alias, too. :-)

Cute.  (For those that don't get the joke, this refers to the "noalias"
keyword that was almost added by the ANSI C committee.

--
Joe Buck
jbuck@galileo.berkeley.edu	 {uunet,ucbvax}!galileo.berkeley.edu!jbuck	

jimad@microsoft.UUCP (Jim ADCOCK) (12/28/90)

In article <NGO.90Dec14093655@tammy.harvard.edu> ngo@tammy.harvard.edu (Tom Ngo)
writes about ~const [not-const] and ~volatile [not-volatile].  

I like this idea, and suggest below further "flavours" that it should be 
possible to revoke or invert through negation.

----

I think ~const would be a good idea, iff we use this opportunity to clean
up the problems with cast-from-const.  Namely, if we have a supportive way
to do caching "correctly" without cast-from-const, then we can take the
opportunity to make cast-from-const a deprecated, strictly "implementation
dependent" construct -- a construct which is not guaranteed to work as you
intend in all situations on all compilers.  This would allow optimizing 
compilers a chance to do meaningful optimizations on const member functions
invoked on const objects, and would allow compilers to at least generate
warnings on casts-from-const that appear to have little chance of working
correctly.

What I am suggesting then, is that the statement in 5.4
[regards an object cast from const] be changed slightly to:

"The result of attempting to modify that [const] object through such a 
[non-const] pointer or reference is implementation dependent.  Possible results 
include acting as if the object were originally declare non-constant, 
or addressing exceptions or other undesired behaviors."

and the language in 7.1.6 be changed slightly to:

"A const object of a type that does not have a constructor or a destructor
might be placed in readonly memory.  A const object that does have a constructor
might be placed in discardable memory -- virtual memory that is only paged
to disk the first time faulted out after construction.  Therefor, the effect 
of any write operation to any const part of any such object is undefined."

[constructed constant bit maps would be good candidates for such discardable 
memory]

|[-] Nothing can be done with the ~const specifier that can't
|    already be done with the existing constructs.  [+] On the other
|    hand, there are other things in C++ that are really just syntactic
|    sugar.  For instance, there is really no difference between a
|    reference "X&" and a constant pointer "X *const", besides the 
|    obvious syntactic difference.

IE in the typical example case where you have a const object except for
one or two caching member variables, one could just write two intermediate 
classes -- one with non const fields, one with all fields const except the 
caching member varibles?  Yuck.  Or are you saying that today one is guaranteed
to be able to cast from const?  -- I claim not.

This is another example of "enabling" verses "supporting."  It seems that
the issue of caching member variable has caused hassles often enough that
C++ ought to support it, not just "enable" it.  Having explicit support for
non-const members within const objects then lets us clean up the issue of
cast from const, and clearly label such as "implementation dependent" in
all cases.

Other examples of supporting verses enabling in C++ is MI support, verses
having to explicitly linearize every method to combine the functionality
of two classes.  Or having overloadable operator dot to support delegation,
smart references, etc, verses having to write a forwarding class that 
trivially re-derives all methods of a class.  Or having access adjustments
so that individual members can be adjusted from a default access, rather than
having to specify the access of each member individually.

|    If this usage of ~const were to be adopted, we would be obligated
|    to consider a similar type-specifier, ~volatile, whose purpose is
|    to remove the volatile attribute from an unknown type.

What's the status of const volatile?  Does the ~'s then imply we also
have ~const volatile, const ~volatile, ~const ~volatile .... ????

....if const volatile is a relatively rare construct, then then inverting
messiness above is not a big deal -- inverting a const volatile flavor
is a rare action taken on an already rare construct.

I support this ~const idea.  I think it helps resolve a lot of the
"does const mean const?" debates.

----

Further implication [in my mind] :

If you can undo const and volatile flavors, maybe you ought to be able to 
undo other default flavors explicitly:

~virtual [not virtual]
~inline  [not inline]

In particular, then one could add a ~inline flavor to one particular
method declared implicitly inline in a class declaration -- to aid debugging
that one method.  You wouldn't have to hack your class to move that one
method being debugged from the .h file to the .c file.

A complete list of keywords that I think could be possible candidates for
negation is:

~const		do allow this object to be changed after init.
~inline		do make this method outline -- IE a real function call.
~register	do not enregister this object, not even when optimizing
			[similar to volatile?]
~virtual 	do not put this method in the vtable.
			[keep the inherited vtable entry?]
~volatile	do allow optimization on this object.

Again, C++ does "enable" work arounds for all this negations.  Typically the
work around entails not specifying a flavor in a superclass, derive two
subclasses, one with the flavor, and one without, etc.  "Obviously", 
encapsulation and reusability are served if flavours specified in the 
superclass can be negated by sub-classes, without having to hack the
super class.

Comments?

pcg@cs.aber.ac.uk (Piercarlo Grandi) (01/03/91)

On 27 Dec 90 19:56:22 GMT, jimad@microsoft.UUCP (Jim ADCOCK) said:

jimad> A complete list of keywords that I think could be possible candidates for
jimad> negation is:

jimad> [ ... ]
jimad> ~inline		do make this method outline -- IE a real function call.
jimad> [ ... ]

Let me home in on this. One problem of the current definition of
'inline' is that it specifies that every time the given function is
called, it should be inlined. In other words, 'inline' currently means
'can inline, *must* inline'. The problem I see with this is that, for
one of the two common reasons one wants to inline, in many cases
expanding inline a function is irrelevant -- it is only productive in a
few places, e.g. in inner loops, or where substantial opportunities for
code merging exist. Inlining a function everywhere, unless it is very
small, is just going to increase size with little benefit in speed.

What is probably needed (not for C++ though) is a distinction between
'inlinable' and 'inline', where the first is used
--
Piercarlo Grandi                   | ARPA: pcg%uk.ac.aber.cs@nsfnet-relay.ac.uk
Dept of CS, UCW Aberystwyth        | UUCP: ...!mcsun!ukc!aber-cs!pcg
Penglais, Aberystwyth SY23 3BZ, UK | INET: pcg@cs.aber.ac.uk

pallas@eng.sun.com (Joseph Pallas) (01/05/91)

In <PCG.91Jan3145302@teachk.cs.aber.ac.uk> pcg@cs.aber.ac.uk
(Piercarlo Grandi) writes:

>One problem of the current definition of
>'inline' is that it specifies that every time the given function is
>called, it should be inlined. In other words, 'inline' currently means
>'can inline, *must* inline'. 

This is entirely wrong.  Unless the standard has changed the language
or intent of the ARM, 'inline' is a hint, and nothing more.

See p. 99 of E&S.  Complain about particular implementations to their
vendors.

joe

jimad@microsoft.UUCP (Jim ADCOCK) (01/08/91)

In article <PCG.91Jan3145302@teachk.cs.aber.ac.uk> pcg@cs.aber.ac.uk (Piercarlo Grandi) writes:
|On 27 Dec 90 19:56:22 GMT, jimad@microsoft.UUCP (Jim ADCOCK) said:
|
|jimad> A complete list of keywords that I think could be possible candidates for
|jimad> negation is:
|
|jimad> [ ... ]
|jimad> ~inline		do make this method outline -- IE a real function call.
|jimad> [ ... ]
|
|Let me home in on this. One problem of the current definition of
|'inline' is that it specifies that every time the given function is
|called, it should be inlined. In other words, 'inline' currently means
|'can inline, *must* inline'. The problem I see with this is that, for
|one of the two common reasons one wants to inline, in many cases
|expanding inline a function is irrelevant -- it is only productive in a
|few places, e.g. in inner loops, or where substantial opportunities for
|code merging exist. Inlining a function everywhere, unless it is very
|small, is just going to increase size with little benefit in speed.

I disagree.  I follow the suggested practice of using inline functions
wherever possible where macros might have been used in the
past.  Inlines are preferable because they are more type safe, and 
provide better compile time error messages.  If these functions are not
inlined, my code will double or triple in size.  Also, I use
inline functions where the intent is that the compiler "customize"
code for a particular application.  In order for "customization" to happen,
the code must be inlined, or again several times larger, slower code will occur.

In my usage, fairly large inline functions are designed with the intent
to generate a small amount of customized inline code, whereas a outlined,
uncustomizable version of the code would be both much larger and much slower.

However, these inlines can be a bear to debug.  And I can't afford to make
all my inline functions outline for debugging, since several times larger
slower "uncustomized" code will result.  Therefor, what I need is the 
ability to simply and easily specify a certain small set of functions
to not be inlined.  ~inline would be one such an approach.  Another,
less elegant approach would be to use some kind of #pragma not inline,
or the such.

ksand@Apple.COM (Kent Sandvik) (01/08/91)

>|On 27 Dec 90 19:56:22 GMT, jimad@microsoft.UUCP (Jim ADCOCK) said:
>|Let me home in on this. One problem of the current definition of
>|'inline' is that it specifies that every time the given function is
>|called, it should be inlined. In other words, 'inline' currently means
>|'can inline, *must* inline'. The problem I see with this is that, for
>|one of the two common reasons one wants to inline, in many cases
>|expanding inline a function is irrelevant -- it is only productive in a
>|few places, e.g. in inner loops, or where substantial opportunities for
>|code merging exist. Inlining a function everywhere, unless it is very
>|small, is just going to increase size with little benefit in speed.


The danger with inlines and compiler optimization is not very high, mostly
because non-inlined code suffers mostly concerning performance.

A keyword like volatile in ANSI C is far more dangerous, because
assumptions of register use could cause a lot of breakable, unportable code. 

So if a compiler does not do inline will not break code, but if the 
compiler does not follow the volatile keywords, then it would cause
a lot of register-dependent code to fail. I hope standards creators
take things like this into account concerning 'hint' keywords.

Kent Sandvik






-- 
Kent Sandvik, Apple Computer Inc, Developer Technical Support
NET:ksand@apple.com, AppleLink: KSAND  DISCLAIMER: Private mumbo-jumbo
Zippy++ says: "C++, anything less is BCPL..."