www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Re: const(FAQ)

reply Kevin Bealer <kevinbealer gmail.com> writes:
guslay Wrote:

 Kevin Bealer Wrote:
 
 guslay Wrote
 ... logical const ...

I still don't see how dividing a const composite object (ie a class) in two separate parts (mutable and immutable) would prevent any optimization or thwart future development. I can see the optimization benefit of an eventual pure qualifier. With no possible side effects, it would allow crazy functional programming tricks (reordering, parallel dispatch, memoizing). But const (or invariant) doesn't mean pure. The following does compile, and is bitwise const. It's legal and it's full of side effects.

But only because it reaches outside the object. For safety / correctness, I could argue that if you want the "dim" data to be const, you need to declare it as such, or wrap it in an object and declare a const one of those, but I guess that's beside the point. You may be right about optimization and purity.
 static int dim = 0;
 class Vector
 {
      public const int size()
      {
          return ++.dim;
      }
 
      public const void printElement( int i )
      {
          // print element i to stdout
      }
 };
 
 As opposed to pure function, the only "mathematical" guarantee that the
 const qualifiers gives on a method is that its immutable bits will not be
 modified. Nothing else. So what is the problem if I say that only half the
 bits are immutable?

(You can, technically, by making half of the fields const individually.) The question is, what does "const" mean -- if it means that all the bits are immutable, you get a certain benefit. If it means that some of them are, you get a different benefit. I think the idea is that the "all bits" guarantee is more useful for the purpose of reasoning about whether it is safe to call a method from inside / outside a locked / synchronized() section. A normal .size() method will operate on the fields of the object in question. If it is const, it does not need to modify those fields. This means that you can safely call such methods in parallel from multiple threads on the same object. Is this an absolute guarantee? No -- side effects as shown above break this assumption. But only the external ".dim" field can be corrupted, and it should have its own object and/or locking if that is an issue. But calling multiple non-const methods is not safe unless proper locking is performed. Now here is the critical distinction: this applies to the D concept of const, but not the C++ concept since mutable fields tend to reintroduce the need for locking even in the presence of C++ "logical const" correctness. The caller may need to get locks when calling the callee's methods (if the callee does not do its own locking), but only for non-const objects. If the callee does its own locking then another problems arises -- the caller must be careful of deadlock. So you are hemmed in from both directions -- it is dangerous to do too much locking (due to deadlock) or too little (due to race conditions), which is why a compiler-enforced guarantee is useful. In either case, knowing the object is const will be more useful if it is an enforced transitive const rather than a logical const, since the former implies something about the need for locking while the latter does not. (Of course, this assumes again that const methods are pure, i.e. don't reach outside the object to change things in important ways. It does require that much cooperation at any rate, although I guess I'm the one to suggest doing otherwise...)
 Const/invariant on data enables aggressive optimizations; const/invariant
 on a method does not.

Probably true.
 Or am I missing something?

I think in C++ you have a kind of contract with the std library, other developers, etc, to not change the observable value of a const object so that it is different after the call than before it. In D you have a similar contract not to change the bitwise value of the object. In C++ this contract is non-enforceable -- if you make all the fields mutable, you have essentially discarded the const qualifier. In D you have a different contract, to not modify the actual bits of an object. This contract is enforceable and solid because the compiler can truly enforce it. The code we've been discussing violates the (C++) contract in any case because it changes the conceptual value of the object from a const method. The compiler can't enforce this because the ++.dim is indistinguishable from the ++ inside a buffered stream when you output a character, or any other external effects. It doesn't violate the D "const contract", because the D contract talks about the bit state of this object (and no other) rather than the observable value. D has a const contract that the compiler really can enforce, and do so completely, whereas C++ has a const contract that the compiler can't enforce. It can make sure you fill out the proper paperwork when you ignore the contract, but it doesn't stop you from ignoring it. Kevin
Mar 31 2008
parent reply guslay <guslay gmail.com> writes:
I think we are agreeing on the benefits of "pure" methods.

But arguing that an immutable object is automatically thread-safe is simply not
true under the current definition of const. It's an over-generalization. It's
just one part of the puzzle.

A pure methods has to be const, but a const method is not implicitly pure.


Kevin Bealer Wrote:

 guslay Wrote:
 
 Kevin Bealer Wrote:
 
 guslay Wrote
 ... logical const ...

I still don't see how dividing a const composite object (ie a class) in two separate parts (mutable and immutable) would prevent any optimization or thwart future development. I can see the optimization benefit of an eventual pure qualifier. With no possible side effects, it would allow crazy functional programming tricks (reordering, parallel dispatch, memoizing). But const (or invariant) doesn't mean pure. The following does compile, and is bitwise const. It's legal and it's full of side effects.

But only because it reaches outside the object. For safety / correctness, I could argue that if you want the "dim" data to be const, you need to declare it as such, or wrap it in an object and declare a const one of those, but I guess that's beside the point. You may be right about optimization and purity.
 static int dim = 0;
 class Vector
 {
      public const int size()
      {
          return ++.dim;
      }
 
      public const void printElement( int i )
      {
          // print element i to stdout
      }
 };
 
 As opposed to pure function, the only "mathematical" guarantee that the
 const qualifiers gives on a method is that its immutable bits will not be
 modified. Nothing else. So what is the problem if I say that only half the
 bits are immutable?

(You can, technically, by making half of the fields const individually.) The question is, what does "const" mean -- if it means that all the bits are immutable, you get a certain benefit. If it means that some of them are, you get a different benefit. I think the idea is that the "all bits" guarantee is more useful for the purpose of reasoning about whether it is safe to call a method from inside / outside a locked / synchronized() section. A normal .size() method will operate on the fields of the object in question. If it is const, it does not need to modify those fields. This means that you can safely call such methods in parallel from multiple threads on the same object. Is this an absolute guarantee? No -- side effects as shown above break this assumption. But only the external ".dim" field can be corrupted, and it should have its own object and/or locking if that is an issue. But calling multiple non-const methods is not safe unless proper locking is performed. Now here is the critical distinction: this applies to the D concept of const, but not the C++ concept since mutable fields tend to reintroduce the need for locking even in the presence of C++ "logical const" correctness. The caller may need to get locks when calling the callee's methods (if the callee does not do its own locking), but only for non-const objects. If the callee does its own locking then another problems arises -- the caller must be careful of deadlock. So you are hemmed in from both directions -- it is dangerous to do too much locking (due to deadlock) or too little (due to race conditions), which is why a compiler-enforced guarantee is useful. In either case, knowing the object is const will be more useful if it is an enforced transitive const rather than a logical const, since the former implies something about the need for locking while the latter does not. (Of course, this assumes again that const methods are pure, i.e. don't reach outside the object to change things in important ways. It does require that much cooperation at any rate, although I guess I'm the one to suggest doing otherwise...)
 Const/invariant on data enables aggressive optimizations; const/invariant
 on a method does not.

Probably true.
 Or am I missing something?

I think in C++ you have a kind of contract with the std library, other developers, etc, to not change the observable value of a const object so that it is different after the call than before it. In D you have a similar contract not to change the bitwise value of the object. In C++ this contract is non-enforceable -- if you make all the fields mutable, you have essentially discarded the const qualifier. In D you have a different contract, to not modify the actual bits of an object. This contract is enforceable and solid because the compiler can truly enforce it. The code we've been discussing violates the (C++) contract in any case because it changes the conceptual value of the object from a const method. The compiler can't enforce this because the ++.dim is indistinguishable from the ++ inside a buffered stream when you output a character, or any other external effects. It doesn't violate the D "const contract", because the D contract talks about the bit state of this object (and no other) rather than the observable value. D has a const contract that the compiler really can enforce, and do so completely, whereas C++ has a const contract that the compiler can't enforce. It can make sure you fill out the proper paperwork when you ignore the contract, but it doesn't stop you from ignoring it. Kevin

Mar 31 2008
next sibling parent reply Kevin Bealer <kevinbealer gmail.com> writes:
guslay Wrote:

 I think we are agreeing on the benefits of "pure" methods.
 
 But arguing that an immutable object is automatically thread-safe is simply
not true under the current definition of const. It's an over-generalization.
It's just one part of the puzzle.
 
 A pure methods has to be const, but a const method is not implicitly pure.

I think what you're saying is right, and I think we can probably stop arguing since I think there is wisdom in each approach.... I think. In fact I could suggest a fourth variety of D const that worked like C++ const, but since there are already at least 3 of them, I would probably get rotten fruit thrown at me ... so I won't. Regarding the D approach then: I think it is a question of cooperation and goals. If you have a method in D that only operates on in-class data then it is pure. (It can also operates on other classes but only if it only calls methods following the same "pure" rules, and they do the same, recursively.) Many methods like .size() can easily be pure since they operate entirely within the object, so in practice this is quite feasible. The D const system does not prove purity, but it helps a lot when writing code and making it pure. The C++ const gives every class the tools and therefore implicitly the permission to use mutable fields in const objects, which means that if people do things in a non-pure way its not even wrong. In fact my (limited) understanding is that the C++ standard actually supports the idea of casting away const just to side step the const regime. It's kind of like garbage collection -- you can have garbage collection in C++ but its risky because it has to be conservative and no existing class expects to run under a GC regime. In D its a conservative system in just the same way, but because it is part of the language, the assumption and the APIs suggest that you shouldn't do certain things. So in D, if you do something that not GC compatible, breaking the code, then its *your fault*, whereas in C++ you could justifiably ask "what do you mean, GC?" If D can establish an understanding (i.e. best practices) that say "don't use mutable external stuff from a const method in a way that impedes purity" then there is a real possibility for more multithread-safe code. There has to be understanding that if int x; class ... { ... const int size() { return ++.x; } }; does not work, its the authors fault. But maybe a full-fledged "pure" is a better approach, but that would prevent a lot of things like output to "stdout", which is not pure but which can be proven not to deadlock. Kevin
Mar 31 2008
parent reply Jason House <jason.james.house gmail.com> writes:
Kevin Bealer Wrote: 
 Regarding the D approach then:
 
 I think it is a question of cooperation and goals.  If you have a method in D
that only
 operates on in-class data then it is pure.  (It can also operates on other
classes but
 only if it only calls methods following the same "pure" rules, and they do the
same,
 recursively.)  Many methods like .size() can easily be pure since they operate
entirely
 within the object, so in practice this is quite feasible.
 
 The D const system does not prove purity, but it helps a lot when writing code
and
 making it pure.  The C++ const gives every class the tools and therefore
implicitly 
 the permission to use mutable fields in const objects, which means that if
people
 do things in a non-pure way its not even wrong.  In fact my (limited)
understanding
 is that the C++ standard actually supports the idea of casting away const just
to
 side step the const regime.

This is actually a good way to think about this stuff. My current problem with the const regime is that global variables can be used to break the transitive const nature. For me to truly feel comfortable with the idea, I need to see equal handling of member variables and global variables. I really hate to see designs that encourage use of global variables! Personally, I like the idea of specifying some kind of exception to the transitive const such as an output-only object.
Apr 01 2008
parent Jason House <jason.james.house gmail.com> writes:
Janice Caron Wrote:

 On 01/04/2008, Jason House <jason.james.house gmail.com> wrote:
 This is actually a good way to think about this stuff.  My current problem
with the const regime is that global variables can be used to break the
transitive const nature.

I think you are incorrect. In a class such as the following int g; class C { int * p; this() { p = &g; } } Then g cannot be modified through a const(C). const C c = new C; *c.g = 4; /*ERROR*/ What you're talking about is something different. You're talking about a member function which is declared const accessing global variables. (Or at least, so I assume). Well, that's not a violation of anything. A /const/ function gets a read-only view of "this", but can still read and write global variables. A /pure/ function cannot see global variables at all.

You're right, I'm talking about a non-pure const function having access to global variables.
  I really hate to see designs that encourage use of global variables!

Every (non-pure) function ever can have some paramters const and others not. For member functions, one of those parameters happens to be "this", but that has no bearing on whether or not global variables are seen as const. Nothing "encourages" you to use global variables. However, rest assured that in the future, it will be possible to declare functions "pure", which will render global variables completely inaccessible.

I have to disagree. Global variables are a back door that allows a head-const-like behavior. This could include classic logging examples or objects to draw on a screen. Using a global variable can achieve this stuff, but in many cases that's bad design. What if I want my logging to be done differently depending on which object does the logging? Or different objects to be drawn to different locations? It's the asymmetry that irks me. How is access to a global variable any different than access to a member object (with a final reference)?
Apr 01 2008
prev sibling parent "Janice Caron" <caron800 googlemail.com> writes:
On 01/04/2008, Jason House <jason.james.house gmail.com> wrote:
 This is actually a good way to think about this stuff.  My current problem
with the const regime is that global variables can be used to break the
transitive const nature.

I think you are incorrect. In a class such as the following int g; class C { int * p; this() { p = &g; } } Then g cannot be modified through a const(C). const C c = new C; *c.g = 4; /*ERROR*/ What you're talking about is something different. You're talking about a member function which is declared const accessing global variables. (Or at least, so I assume). Well, that's not a violation of anything. A /const/ function gets a read-only view of "this", but can still read and write global variables. A /pure/ function cannot see global variables at all.
  I really hate to see designs that encourage use of global variables!

Every (non-pure) function ever can have some paramters const and others not. For member functions, one of those parameters happens to be "this", but that has no bearing on whether or not global variables are seen as const. Nothing "encourages" you to use global variables. However, rest assured that in the future, it will be possible to declare functions "pure", which will render global variables completely inaccessible.
Apr 01 2008