On 9/12/07, Walter Bright <newshound1@digitalmars.com> wrote:
Janice Caron wrote:
> So the function was declared const, because /conceptually/ it was.
>
> But the class had a mutable cache, declared with the C++ keyword "mutable"
>
> Transitivity would wreck that.

You're right in that transitive const does not support the notion of
"logical const" (which is the usual term for what you are referring to).

The problem with logical const, however, is it offers no semantic
guarantees. I pass a logical const reference around, and the underlying
data may or may not change. I have no guarantees one way or the other,

But you could guarantee that /non-private/ data will not change. And since non-private data is never seen outside the file in which it is declared, I don't understand why this is a problem.
 

and even worse, I can't even tell this is happening. "I" here meaning
the compiler, and our hapless code auditor.

Providing you restrict "logical constness" to private variables, is that really true? Inside the module, the compiler knows everything. Outside the module, the private variables are irrelevant anyway as they can never be accessed in any way.
 

This just pulls the rug out from under:

1) functional programming

Functional programming isn't possible anyway unless the function can also guarantee that it won't modify /global/ variables, and I see no way of specifying that in the prototype.

 

2) multithreaded programming

Likewise, a function cannot be guaranteed to be completely threadsafe unless it guarantees not to modify global variables, and I see no way of specifying that in the prototype.

Even if you could, it's more complicated than that. A function might modify global variables and yet still be threadsafe - providing it uses mutexes to ensure it has exclusive access to the shared variables. A good example of this is writef(). The writef() function writes to standard output, and so /must/ modify some global state - but it's thread-safe because file access is all mutex locked.

Moreover, multithreaded programming might /require/ you to lock a mutex, do something, then unlock said mutex. The mutex itself needs to be modifiable - that is, "logically const".

More thought needs to be put into that one.

 

3) having a tightly specified interface

Interfaces are my concern. If an Interface specifies that a function be const (in the sense of non modifying member variables), then it would make a lot of sense that classes which implement that interface be allowed to assume the interface means "logical constness", for the reasons of all the use-cases given so far.

 

It goes back to painting a stripe across your hips and calling it a
seatbelt.

Given this, it isn't any surprise that C++ is disastrously difficult to
write functional & multithreaded programs in, and C++ mutability is one
of the reasons why. In C++, you can write const this and const that and
it doesn't mean jack squat. Many experienced C++ programmers will tell
you that const is little more than a documentation aid.

I /am/ an experienced C++ programmer, and I agree. However, in C++, it is possible to declare non-private members mutable - and to make matters worse, the function bodies of functions which modify that variable could be in any number of different source files scattered all over the place.

In D it's different. First off, I do suggest restricting "logical constness" to private variables only. Secondly, all the function defintions which can access those source files are in one file, so the compiler knows everything.

This means that, in D, when you declare a function as const, there would still be an absolute guarantee that nothing non-private could ever be changed by that function. Outside the module, that's all you need to know. Inside the module, the compiler knows all anyway.



In other words, you're quite right that transitive const totally wrecks
using const to specify logical constness. And that's a good thing <g>.

But logical constness is a /necessary/ thing, and if you outlaw any way of doing it legitimately, then people will do it by "cheating". By (for example), storing state in global variables. The simplest example I can come up with is that random number one. Recall:

 class RandomNumberGenerator;
 {
     private long seed;
    
     const int rand() /* guarantees not to modify any non-private member variables */
     {
         seed = f(seed);
         return (seed >> 32);
     }
 }

So then I'd have to change it to something like:

 private long seed;

 private long getSeed()
 {
     lockMutex();
     long s = seed;
     unlockMutex();
     return s;
 }

 class RandomNumberGenerator;
 {
     const int rand()
     {
         seed = f(getSeed());
         return (seed >> 32);
     }
 }


And I'd end up with something that was much nastier than the very thing you are trying to avoid.