[comp.lang.c++] const is not object-oriented

linton@sgi.com (Mark Linton) (11/10/90)

The "const" keyword in C++ specifies a property of storage, and
as such is not particularly useful in an object-oriented program.
Declaring a parameter or a member function as const requires
knowledge about the implementation of a class.  Consider
a class IntVector with a member function sum:

    class IntVector {
    public:
	IntVector(unsigned int size);
	~IntVector();

	int get(unsigned int i);	// return element i
	void set(unsigned int i, int v);// set element i to v
	int sum();
    private:
	int* data;
	unsigned nelements;
    }

The function "sum" is to return the add up all the elements in
the vector.  One might think it would be natural to define sum as
a const member function, which would allow a call to sum
on a const IntVector object (such as a const IntVector parameter).
After all, computing the sum of the elements of a vectors
doesn't "change" the vector.  Suppose, however, that sum will be
called many times before the vector is modified.  Because you know
the vector is only modified by set, you could add two members
to the private section:

    int current_sum;
    boolean sum_is_valid;

The set member function would set sum_is_valid to false, and sum
would check the flag to determine whether it needs to compute the sum
or simply use the previously computed value.

If you implement this caching strategy, you can no longer make sum
a const member function!  The great irony is that you could define
set as a const member function because it modifies data pointed at
by the vector object, not the vector's own structure.

So, I claim that const is a pretty useless concept in class interfaces
because it depends on the class implementation.  I can see the use
of const for concrete types (const char*), or perhaps in very special
cases for optimization (like for helping a vectorizing compiler
handle a parallel program).

There is one place where the notion of consts confuses compiler writers.
Cfront, for example, generates a warning if you pass a temporary
to a non-const ref parameter.  For example,

    class A {
    public:
	A(int);

	int f(A&);
    };

    A* a;

    a->f(A(4));

This call will generate a warning from cfront.  I believe this warning
is out-right wrong because it will confuse programmers into believing that
they should change the declaration of f to "int f(const A&)".  However,
if the function f is implemented using caching, they can't do it.
So the code must be split up:

    A tmp(4);
    a->f(tmp);


The bottom line is if you are defining a C++ class, especially
for a library, do not use const parameters or const member functions.
Const means storage, not behavior, so it is by definition
an implementation/representation concept.  If you are a compiler writer,
do not generate this bogus warning when a temporary is passed
to a non-const ref parameter.

pal@xanadu.wpd.sgi.com (Anil Pal) (11/10/90)

In article <1990Nov9.181408.23110@odin.corp.sgi.com>, linton@sgi.com (Mark Linton) writes:
|> 
|> The "const" keyword in C++ specifies a property of storage, and
|> as such is not particularly useful in an object-oriented program.
|> 
|> [ example of caching result of expensive computation that does not affect
|>   object state ; this prevents member function being declared "const" ]
|> 
|> So, I claim that const is a pretty useless concept in class interfaces
|> because it depends on the class implementation.

I would disagree with the claim that const is useless in class
interface definition.  The distinction you note (between "logical"
constness and "bitwise" const) is a valid one, and has come up on this
group before.

My position is that use of const in an interface definition indicates
intent, i.e. the logical or behavioral const.  I contend that this
information is useful to users of the class.

Since the translator cannot, with current technology, enforce a purely
logical const it seems reasonable for it to enforce the more
restrictive bitwise const notion instead, since the class implementor
can circumvent this when necessary.

I will concede that this is a mismatch, but claim that the answer is
not to take const as a storage specifier and throw it away (thereby
losing the interface benefits it provides).

Rather, I would suggest that const be used in the behavioral sense to
specify interfaces, and, where necessary, the translator's more
restrictive notion of storage const can be circumvented in the
implementation.

|> 
|> Cfront, for example, generates a warning if you pass a temporary
|> to a non-const ref parameter.
|>     a->f(A(4));
|> I believe this warning is out-right wrong[...] [because]
|> if the function f is implemented using caching, they can't [ declare the
|> parameter const]

I assume you mean that the function f is implemented using calls to
member functions of the parameter that use caching, rather than that
the function caches values in a; in the latter case, there is no
problem with declaring the parameter const.

The restriction now is that the member functions invoked on the
parameter be declared const, even if they involve caching.  This is
possible, although it requires that the constness be circumvented
within the function.  I will concede that this can be ugly (const cast
away warnings and all that), but it could also be done indirectly (by
having a pointer to the cached value, for example).  In any case, I
contend that the constness of the reference parameter and the constness
of the member function are important parts of the interface definition,
and should be included.

There is, of course, some work involved here, but I believe it is
(appropriately) confined to the implementor of the class.

|> 
|> The bottom line is if you are defining a C++ class, especially
|> for a library, do not use const parameters or const member functions.
|> Const means storage, not behavior, so it is by definition
|> an implementation/representation concept.  If you are a compiler writer,
|> do not generate this bogus warning when a temporary is passed
|> to a non-const ref parameter.
|> 

Again, I have to disagree.  I believe that const should be used in
interfaces, in the behavioral sense.  The fact that the translator is
limited to treating it as a storage concept imposes an implementation
cost, but I contend that the benefits of having tightly specified
interfaces outweigh that cost.  This is particularly true for
libraries.  I have had lots of problems with libraries that do not
declare parameters const, because I am never sure whether I can safely
pass a constant (a string literal, for example), a computed temporary
expression, etc.

On my previous project, we moved a fairly large program (25K lines)
from C++ 1.2 to 2.0, and decided to tighten up the interfaces with
const.  It turned out to be a much larger underatking than we
anticipated, because of the very stringent restrictions that storage
const implies.  However, the result was definitely a much cleaner and
more understandable program.

-- 
Anil A. Pal,	Silicon Graphics, Inc.
pal@sgi.com	(415)-335-7279

rgreen@bbn.com (Robert W. Green) (11/10/90)

In article <1990Nov9.181408.23110@odin.corp.sgi.com> linton@sgi.com (Mark Linton) writes:
>The "const" keyword in C++ specifies a property of storage, and
>as such is not particularly useful in an object-oriented program.
>Declaring a parameter or a member function as const requires
>knowledge about the implementation of a class.

> ... example deleted

>The bottom line is if you are defining a C++ class, especially
>for a library, do not use const parameters or const member functions.
>Const means storage, not behavior, so it is by definition
>an implementation/representation concept.

Whoa, I think you are throwing out the baby with the bath water. The ability to
declare methods as const is just too valuable for the design of large systems to
just throw out. The language specification may need some tuning but I wouldn't
just toss it. Consider your example. It can be implemented using a cast of the
form:

        ((IntVector *const) this)->current_sum = /* sum calculation */

This works with g++. I haven't tried it with cfront but I think it should be
legal c++ code (I lifted the type spec. from page 177 of E&S which defines the
declaration of this).

Now I would agree that use of a cast is fairly ugly but I have difficulty
thinking of a mechanism which doesn't paper over a similiar hole in the
type system. As ugly as it is, an occasional cast is a big improvement
over dropping use of const everywhere.

-Bob

PS. I have implemented a class which does exactly the caching of statistical
calculations in your example. However, in my implementation, the cached
statistics are maintained as a side effect of the update methods defined for the
data vector.  Since changing the data vector is by definition not a const member
function, I was able to update the cached statistics without violating the type
system or using a ugly cast. Again, dropping use of const seems to be overkill.

Bruce.Hoult@actrix.co.nz (Bruce Hoult) (11/11/90)

>If you implement this caching strategy, you can no longer make sum
>a const member function!
 
I think this is a clear case of the programmer knowing better than the
compiler and being justified in using a cast of "this" to IntVector* in
sum() at the point at which sum_is_valid is changed.

vaughan@mcc.com (Paul Vaughan) (11/11/90)

   From: linton@sgi.com (Mark Linton)
   Newsgroups: comp.lang.c++,comp.std.c++
   Date: 9 Nov 90 18:14:08 GMT
   Reply-To: linton@sgi.com (Mark Linton)
   Organization: sgi
   Lines: 78

   The "const" keyword in C++ specifies a property of storage, and
   as such is not particularly useful in an object-oriented program.

Hm, const is also used to declare properties of pointers.  Having a 

const IntVector* p;

doesn't imply that whatever p points to can't be changed.  It simply
means that it can't be changed using p.  My impression is that const
really only specifies a property of storage in the declaration of a
file scoped object.  Even then, I'm not too sure.

   Declaring a parameter or a member function as const requires
   knowledge about the implementation of a class.  Consider
   a class IntVector with a member function sum:


       class IntVector {
       public:
	   IntVector(unsigned int size);
	   ~IntVector();

	   int get(unsigned int i);	// return element i
	   void set(unsigned int i, int v);// set element i to v
	   int sum();
       private:
	   int* data;
	   unsigned nelements;
       }

   The function "sum" is to return the add up all the elements in
   the vector.  One might think it would be natural to define sum as
   a const member function, which would allow a call to sum
   on a const IntVector object (such as a const IntVector parameter).
   After all, computing the sum of the elements of a vectors
   doesn't "change" the vector.  Suppose, however, that sum will be
   called many times before the vector is modified.  Because you know
   the vector is only modified by set, you could add two members
   to the private section:

       int current_sum;
       boolean sum_is_valid;

   The set member function would set sum_is_valid to false, and sum
   would check the flag to determine whether it needs to compute the sum
   or simply use the previously computed value.

This is indeed a problem, but it can be overecome.  My usual approach
to the problem is to use a non-const overloading and a cast, like
this:

	int IntVector::sum() { //add em up, save it, and return };
        int IntVector::sum() const { return ((IntVector*) this)->sum(); }

   If you implement this caching strategy, you can no longer make sum
   a const member function!  The great irony is that you could define
   set as a const member function because it modifies data pointed at
   by the vector object, not the vector's own structure.

Sure.  C++ can't really recognize that an object considers certain
things that its members point to as being part of itself.  This being
the case, another approach to the above problem would be to declare

  int* current_sum;

as a member variable and allocate/delete the space for it in the
constructor/destructor.  But this seems pretty silly.  The bottom line
here is that the compiler can't tell you what should or should not be
const, it can only tell you what cannot be const.

   So, I claim that const is a pretty useless concept in class interfaces
   because it depends on the class implementation.  I can see the use
   of const for concrete types (const char*), or perhaps in very special
   cases for optimization (like for helping a vectorizing compiler
   handle a parallel program).

It need not depend on the class implementation, as shown above.  While
I've generally accepted the idea that declaring things const is useful
for several theoretical reasons, I'd still like to see some empirical
evidence.  It certainly is not without cost.

   There is one place where the notion of consts confuses compiler writers.
   Cfront, for example, generates a warning if you pass a temporary
   to a non-const ref parameter.  For example,

       class A {
       public:
	   A(int);

	   int f(A&);
       };

       A* a;

       a->f(A(4));

   This call will generate a warning from cfront.  I believe this warning
   is out-right wrong because it will confuse programmers into believing that
   they should change the declaration of f to "int f(const A&)".  However,
   if the function f is implemented using caching, they can't do it.
   So the code must be split up:

       A tmp(4);
       a->f(tmp);

The above code generates a syntax error using cfront simply because it
is a fragment.  Also, I'm not sure it is quite the fragment that you
intended.  For instance, this code (which does compile) issues only
the warning shown with cfront:

class A {
public:
  A(int);
  int f(A&);
};


main() {
  A* a;
  
  a->f(A(4));       //warning:  a used but not set
}


I agree that if cfront had produced a non-const warning, it would have
been wrong.  I'm using the cfront 2.0 derivative Sun CC.  Perhaps the
version you are using is broken?  I'll discuss the following code.

class A {
public:
  A(int);
  int f(A&);
};


main() {
  const A* a;
  a->f(A(4));          // generates a const warning
}

This is precisely a case where I would use

class A {
public:
  A(int);
  int f(A&);
  int f(const A&);  // implemented in terms of f(A&), using a cast
};

if the f member function doesn't change the observable state of an A
object.

   The bottom line is if you are defining a C++ class, especially
   for a library, do not use const parameters or const member functions.
   Const means storage, not behavior, so it is by definition
   an implementation/representation concept.

I disagree.  

				If you are a compiler writer,
   do not generate this bogus warning when a temporary is passed
   to a non-const ref parameter.

As far as I can tell, there is no problem here.


 Paul Vaughan, MCC CAD Program | ARPA: vaughan@mcc.com | Phone: [512] 338-3639
 Box 200195, Austin, TX 78720  | UUCP: ...!cs.utexas.edu!milano!cadillac!vaughan

rfg@NCD.COM (Ron Guilmette) (11/12/90)

In article <60760@bbn.BBN.COM> rgreen@spcpv3.bbn.com (Bob Green) writes:
+In article <1990Nov9.181408.23110@odin.corp.sgi.com> linton@sgi.com (Mark Linton) writes:
+>The "const" keyword in C++ specifies a property of storage, and
+>as such is not particularly useful in an object-oriented program.
+>Declaring a parameter or a member function as const requires
+>knowledge about the implementation of a class.
+
+> ... example deleted
+
+>The bottom line is if you are defining a C++ class, especially
+>for a library, do not use const parameters or const member functions.
+>Const means storage, not behavior, so it is by definition
+>an implementation/representation concept.
+
+Whoa, I think you are throwing out the baby with the bath water. The ability to
+declare methods as const is just too valuable for the design of large systems to
+just throw out. The language specification may need some tuning but I wouldn't
+just toss it. Consider your example. It can be implemented using a cast of the
+form:
+
+        ((IntVector *const) this)->current_sum = /* sum calculation */
+
+This works with g++. I haven't tried it with cfront but I think it should be
+legal c++ code (I lifted the type spec. from page 177 of E&S which defines the
+declaration of this).

People need to be aware that although the `casting aside constness' trick
seems to work in most cases, and although it *is* >legal< it is not
necessarily wise to rely on the assumption that the use of this trick will
result in generated code which does what you expect on all current
and future implementations of the language.

If the ANSI C++ committee adopts language similar to that in the ANSI C
standard regarding the semantics of `const' (and I see no reason why
they should not), then the final ANSI C++ standard will say that the
result of modifying a const (indirectly) via a pointer to a non-const
are undefined.

The implication is that some (very sophisticated) optimizers may one
day cause any code which makes use of this `trick' to stop working.

Be advised.

-- 

// Ron Guilmette  -  C++ Entomologist
// Internet: rfg@ncd.com      uucp: ...uunet!lupine!rfg
// Motto:  If it sticks, force it.  If it breaks, it needed replacing anyway.

marc@dumbcat.sf.ca.us (Marco S Hyman) (11/12/90)

In article <1990Nov9.210404.29139@relay.wpd.sgi.com> pal@sgi.com writes:
    On my previous project, we moved a fairly large program (25K lines)
    from C++ 1.2 to 2.0, and decided to tighten up the interfaces with
    const.  It turned out to be a much larger underatking than we
    anticipated, because of the very stringent restrictions that storage
    const implies.  However, the result was definitely a much cleaner and
    more understandable program.

A question from the curious -- did adding const to the interface find any
problems/holes/bugs?  I believe that using const, i.e. forcing a class
designer to think about the constness of each interface, is a GoodThing.  I
also do not doubt that the program was better as a result of the effort to
clean it up.  But was it worth the effort to fix up old, presumably working,
code?

// marc
-- 
// marc@dumbcat.sf.ca.us
// {ames,decwrl,sun}!pacbell!dumbcat!marc

linton@sgi.com (Mark Linton) (11/13/90)

All the responses so far have essentially said the same thing:
"we don't care what const really means, we'll use it how we want".
I understand the motivation, but this is a bit dangerous.
If a compiler puts a static life-time const in read-only storage and
you use a cast to write into that storage, you'll get a memory fault
(speaking from experience here).

It seems clear that const doesn't provide the semantics that many programmers
(including me) want.  We can

    (1) continue to abuse its meaning as many are already,
	using casts to circumvent compiler attempts to enforce the meaning

    (2) change the meaning of const to remove the storage semantics

    (3) find another way to do what we want

(1) is just an informal convention for (2), where the programmer uses
casts to get around the fact that the compiler thinks const has more meaning.
(2) would change const to be a simple subtyping mechanism in that
only const member functions can be used on const objects.  Const would
no longer mean anything with respect to storage.

(3) is a useful exercise to consider.  I can imagine a value for a notion
of const storage, for things like read-only data or vectorizing compilers,
so I'm against (2).  (1) is dangerous because users of classes
will not realize that class designers are not using const as
it is specified.  If the C++ community is really adopting (1), we should
at least put a pragma in the interfaces so that compilers and users
can be aware of this convention.

It seems like the desired behavior of (2) could also be achieved
by defining "const" member functions in a base class and
non-const functions in a subclass.  This is the general mechanism
we use when we want to separate which member functions can be used
in different circumstances.  It is curious that for the case of const
behavior programmers seem to want (need?) another language mechanism.

By the way, for those who couldn't reproduce my cfront problem,
it turns out to be a bit subtler than I realized.  The program below
demonstrates the bogus warning, but if the A(int) constructor
is removed then no warning is generated.  I suppose this should just
be classified as another one of those mysterious cfront bugs.

class A {
public:
    A();
    A(int);

    void f(A&);
};

void g(A& a) {
    a.f(A());
}

pal@xanadu.wpd.sgi.com (Anil Pal) (11/13/90)

In article <1990Nov12.184240.23430@odin.corp.sgi.com>, linton@sgi.com (Mark Linton) writes:

|> If a compiler puts a static life-time const in read-only storage and
|> you use a cast to write into that storage, you'll get a memory fault
|> (speaking from experience here).

Page 109 of E&S states [emphasis added] that 

    "A const object of a type that *does not* have a constructor or a
    destructor may be placed in readonly memory" [...]

    "This implies that most const objects of C++ style class types may
    *not* be placed in readonly memory" [...]

    "The purpose of this distinction is to allow the use of readonly
    memory for large tables, while still enabling "constness" for class
    objects to be defined by the programmer through the definition of
    const member functions"

From this, I infer that the intent of "const" is for it to be semantic
and programmer defined.  The storage notion of const is therefore an
compiler feature, intended to prevent accidental update.  From the
language standpoint, therefore, const already has the semantics you
(and I, and others) want.

The issue then becomes what the compiler should do.

It appears impractical for the compiler to implement the programmer's
notion of const (how is this notion communicated?).  This leaves the
alternatives of doing nothing, or implementing the storage notion of
const, which the programmer can circumvent as necessary.  Are there
other alternatives?

I contend that the cases where storage const is overly restrictive are
few and can be well isolated, making them suitable candidates for
explicit casts.  I definitely prefer this situation to the alternative
where the compiler essentially punts on const enforcement.

-- 
Anil A. Pal,	Silicon Graphics, Inc.
pal@sgi.com	(415)-335-7279

bill@ssd.csd.harris.com (Bill Leonard) (11/15/90)

In article <1990Nov12.184240.23430@odin.corp.sgi.com> linton@sgi.com (Mark Linton) writes:

   It seems clear that const doesn't provide the semantics that many programmers
   (including me) want.  We can

       (1) continue to abuse its meaning as many are already,
           using casts to circumvent compiler attempts to enforce the meaning

       (2) change the meaning of const to remove the storage semantics

       (3) find another way to do what we want

Being rather new to C++, I feel I haven't quite understood all the discussion
about "const".  However, as a compiler writer, I definitely sense something
missing.  The discussion appears to have focused on two aspects of "const":

  1) Whether const objects are placed in some particular type of storage
     (like ROM);

  2) Whether the compiler should "enforce" the prohibition against updates
     that const implies.

Much of the arguments have been about whether "casting away const" achieves
the desired behavior, but ignored in all this has been the compiler's
freedom to optimize in the presence of const.  Let's take an example.
Suppose procedure "foo" is declared:

   int foo (const int * a) ;

and suppose the following code:

   int        x, y ;
   x = 1 ;
   foo (&x) ;
   y = x + 1 ;

Since foo has claimed that it does not modify the storage pointed to by its
parameter, the compiler is free (I believe) to substitute the value 1 for x
in the assignment to y.  If foo "casts away" this const-ness and modifies
the storage anyway, you will get unexpected behavior.

Perhaps I don't properly understand the C++ specification (yes, I know it's
not a standard yet), but I am pretty sure this is the ANSI C model.
Casting away const could really be dangerous and unpredictable if you are
using an optimizing compiler (and why would you want to use anything else?
:-).  _I_ certainly would never use a cast to non-const unless I knew
exactly what the compiler was going to do with it (which, of course, makes
my code non-portable).

I strongly support the notion of const, especially this interpretation of
freedom to optimize.  As a programmer, I want to give the compiler every
opportunity to optimize my program.  As a compiler writer, I want to
discourage people from subverting this goal, because the non-portable
aspects of it can go unnoticed for years.
--
Bill Leonard
Harris Computer Systems Division
2101 W. Cypress Creek Road
Fort Lauderdale, FL  33309
bill@ssd.csd.harris.com
---------------------------------------------------------------------------
"You may have the reins in your hands, but the wagon only goes when the
horses do."
---------------------------------------------------------------------------

gregk@cbnewsm.att.com (gregory.p.kochanski) (11/15/90)

In article <1990Nov12.184240.23430@odin.corp.sgi.com> linton@sgi.com (Mark Linton) writes:
> 
>    It seems clear that const doesn't provide the semantics that many programmers
>    (including me) want.

Here's a possible extension of 'const' which is powerful, not too klugey,
but perhaps a bit too far off to get into ANSI C++:

Const can be split into a storage-description intent:
const int x = 3;

and a intent to describe the interface to a function:
int foo(const int& x, int& y).

First, seperate these meanings.  Use 'const' for the first, and replace
the second meaning with 'condensed function prototypes' (like condensed
soup or condensed books).  In the above simple case, you could
write:
prototype int foo(int& x, int& y) {y=0;}

Which would be shorthand for the fact that foo() modifies 'y', but not 'x'.
Big deal, so you say.  Well, so would I, but in more complex cases, it
works really nicely.  Take the recent discussions on cacheing:

class array_with_cached_sum	{
	double *data;
	double sum_cache;
	public:
	double sum() { ...... }
	};

Here we have a class that is hard to add-up, so we store a sum.
Now, array_with_cached_sum::sum() is declared as follows:
prototype double sum() {sum_cache=0;} // It doesn't change the data...

Functions which just look at the array are declared like this:
prototype double lookie_here(array_with_cached_sum& a) {;} // change nothing

// Only calls sum() on the data.
prototype double lookie_sum(array_with_cached_sum& a) {a.sum();}

And, a function that modifies the data could be
prototype double changit(array_with_cached_sum& a) {*a.data=0;}

or, just
prototype double changall(array_with_cached_sum& a) {a=0;}

So, now compilers have all necessary information to optimize to
their heart's content, users can specify functions that change
only part of an object.
Best of all, if we have a linked list of pointers to objects,
we can now distinguish between
* functions that just look at the list,
* functions that change objects without adding or removing any,
* functions that add or subtract objects from the list, but don't 
	change the objects, and
* functions that do everything imaginable to the list.

Greg Kochanski