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