www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Best practices for logical const

reply "Adam D. Ruppe" <destructionator gmail.com> writes:
D doesn't have logical const, but sometimes it is useful, 
especially with lazy initialization of a field, and we can kinda 
fake it with casts or with global variables. Modifying an 
immutable object is undefined behavior, so how can we avoid that, 
and if not, try to minimize the problems in practice?

Using global variables to store local state is kinda silly, it 
seems to me that doing a AA lookup kinda defeats the point of 
caching in the first place, so I want to focus on the cast method.

So:

* should we always wrap the write in a synchronized block to 
minimize the chances that we'll have thread problems with 
implicitly shared immutable things passed as const? What's the 
best way to do this btw?

* should objects with logical const methods deliberately not 
provide immutable constructors to prevent them from being 
immutable? Would this actually work?

* Anything else that is likely to come up?
Feb 14 2014
next sibling parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Saturday, February 15, 2014 04:03:50 Adam D. Ruppe wrote:
 D doesn't have logical const, but sometimes it is useful,
 especially with lazy initialization of a field, and we can kinda
 fake it with casts or with global variables. Modifying an
 immutable object is undefined behavior, so how can we avoid that,
 and if not, try to minimize the problems in practice?
 
 Using global variables to store local state is kinda silly, it
 seems to me that doing a AA lookup kinda defeats the point of
 caching in the first place, so I want to focus on the cast method.
 
 So:
 
 * should we always wrap the write in a synchronized block to
 minimize the chances that we'll have thread problems with
 implicitly shared immutable things passed as const? What's the
 best way to do this btw?
 
 * should objects with logical const methods deliberately not
 provide immutable constructors to prevent them from being
 immutable? Would this actually work?
 
 * Anything else that is likely to come up?

If you want logical const, don't use const or immutable at all. To do so is undefined as they require physical constness/immutability. So, if you want logical const, you need to indicate constness in some other way that's not defined by the language. Pretty much by definition, you can't have logical const with const or immutable, because the only way to even attempt it involves casts, which means undefined behavior. I'd strongly advise against even considering it, let alone trying it. - Jonathan M Davis
Feb 15 2014
prev sibling next sibling parent "Jakob Ovrum" <jakobovrum gmail.com> writes:
On Saturday, 15 February 2014 at 10:52:25 UTC, Jonathan M Davis 
wrote:
 If you want logical const, don't use const or immutable at all. 
 To do so is
 undefined as they require physical constness/immutability. So, 
 if you want
 logical const, you need to indicate constness in some other way 
 that's not
 defined by the language. Pretty much by definition, you can't 
 have logical
 const with const or immutable, because the only way to even 
 attempt it
 involves casts, which means undefined behavior. I'd strongly 
 advise against
 even considering it, let alone trying it.

 - Jonathan M Davis

+1. Shoe-horning D's const into C++'s const is a bad idea. They are fundamentally different and shouldn't be used the same way. I like to think D's const exists because of immutable and for that reason alone.
Feb 15 2014
prev sibling next sibling parent reply "Peter Alexander" <peter.alexander.au gmail.com> writes:
If you want logical const in D, you just don't use const. If you 
try to hack around it, it will just come back and bite you.

As a result, when writing APIs, don't enforce constness of your 
parameters if you require logical const. e.g. a range will not 
necessarily have const front and empty. Trying to enforce that 
will lead to trouble.
Feb 15 2014
parent Timon Gehr <timon.gehr gmx.ch> writes:
On 02/15/2014 08:34 PM, Jesse Phillips wrote:
 But mutating a mutable address is still valid, so as long as _you_
 guarantee the address is not immutable, no it won't format your harddrive.

class C{ bool doit = true; } void foo(const(C) c)pure{ (cast()C).doit = false; } void main(){ auto c = new C; writeln(c.doit); foo(c); if(c.doit) formatHardDrive(); }
Feb 15 2014
prev sibling next sibling parent "Jesse Phillips" <Jesse.K.Phillips+D gmail.com> writes:
On Saturday, 15 February 2014 at 12:23:54 UTC, Peter Alexander 
wrote:
 If you want logical const in D, you just don't use const. If 
 you try to hack around it, it will just come back and bite you.

 As a result, when writing APIs, don't enforce constness of your 
 parameters if you require logical const. e.g. a range will not 
 necessarily have const front and empty. Trying to enforce that 
 will lead to trouble.

I fear it may not be that simple. Caching tends to be the main example for wanting logical const, but caching may not be the first thing implemented. Write some code, make what you can const, build from that code, make more things const. Go back and implement caching, remove const from everything. To me, the only requirement for implementing logical const is that you make sure it is never implemented on immutable. Casting away const is not undefined, only mutating it afterwards. But mutating a mutable address is still valid, so as long as _you_ guarantee the address is not immutable, no it won't format your harddrive.
Feb 15 2014
prev sibling next sibling parent "Meta" <jared771 gmail.com> writes:
On Saturday, 15 February 2014 at 12:23:54 UTC, Peter Alexander 
wrote:
 If you want logical const in D, you just don't use const. If 
 you try to hack around it, it will just come back and bite you.

 As a result, when writing APIs, don't enforce constness of your 
 parameters if you require logical const. e.g. a range will not 
 necessarily have const front and empty. Trying to enforce that 
 will lead to trouble.

inout is *sort of* logical const, if the underlying type is immutable. T logicalConst(T)(inout(T) t) { //Error: cannot modify inout expression t t += 1; return t; } void main() { auto n = 0; auto m = logicalConst(m); } The only problem is figuring out if T itself is actually mutable and not immutable or const. If you can do that, then it's fine to cast inout away and mutate the value.
Feb 15 2014
prev sibling next sibling parent "Meta" <jared771 gmail.com> writes:
On Saturday, 15 February 2014 at 19:59:07 UTC, Meta wrote:
 inout is *sort of* logical const, if the underlying type is 
 immutable.

I mean mutable, of course.
Feb 15 2014
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Sat, 15 Feb 2014 15:02:44 -0500, Meta <jared771 gmail.com> wrote:

 On Saturday, 15 February 2014 at 19:59:07 UTC, Meta wrote:
 inout is *sort of* logical const, if the underlying type is immutable.

I mean mutable, of course.

No, it's not. It's not allowed to mutate with inout. -Steve
Feb 15 2014
prev sibling next sibling parent "Meta" <jared771 gmail.com> writes:
On Sunday, 16 February 2014 at 02:34:36 UTC, Steven Schveighoffer 
wrote:
 On Sat, 15 Feb 2014 15:02:44 -0500, Meta <jared771 gmail.com> 
 wrote:

 On Saturday, 15 February 2014 at 19:59:07 UTC, Meta wrote:
 inout is *sort of* logical const, if the underlying type is 
 immutable.

I mean mutable, of course.

No, it's not. It's not allowed to mutate with inout. -Steve

Right, but inout can accept any of mutable, const, and mutable. The compiler will statically disallow you from mutating an inout variable, but if you know the underlying data is mutable, it's safe to cast inout away and mutate. If the data is actually mutable, then inout is effectively logical const. I guess the same is true of regular const.
Feb 15 2014
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Fri, 14 Feb 2014 23:03:50 -0500, Adam D. Ruppe  
<destructionator gmail.com> wrote:

 D doesn't have logical const, but sometimes it is useful, especially  
 with lazy initialization of a field, and we can kinda fake it with casts  
 or with global variables. Modifying an immutable object is undefined  
 behavior, so how can we avoid that, and if not, try to minimize the  
 problems in practice?

 Using global variables to store local state is kinda silly, it seems to  
 me that doing a AA lookup kinda defeats the point of caching in the  
 first place, so I want to focus on the cast method.

 So:

 * should we always wrap the write in a synchronized block to minimize  
 the chances that we'll have thread problems with implicitly shared  
 immutable things passed as const? What's the best way to do this btw?

As a start, I would take a look at the code that generates the mutex when you synchronize an object. It is the only case where logical const is allowed in D.
 * should objects with logical const methods deliberately not provide  
 immutable constructors to prevent them from being immutable? Would this  
 actually work?

This is a good idea. Can you disable the immutable constructor?
 * Anything else that is likely to come up?

Where you must be very very cautious is immutable pure functions. The compiler may make assumptions that are not correct in terms of shortcuts. My advice is to never change "logical const" data inside a const pure function. Other than that, depending on the use case, use C threading rules when making changes. Basically, if you know the object will never be referenced in multiple threads (very application specific), then you are OK to just change it. Otherwise, I'd put a mutex on it, or (ironically) use the object's monitor to protect it. -Steve
Feb 15 2014
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Sat, 15 Feb 2014 21:36:43 -0500, Meta <jared771 gmail.com> wrote:

 On Sunday, 16 February 2014 at 02:34:36 UTC, Steven Schveighoffer wrote:
 On Sat, 15 Feb 2014 15:02:44 -0500, Meta <jared771 gmail.com> wrote:

 On Saturday, 15 February 2014 at 19:59:07 UTC, Meta wrote:
 inout is *sort of* logical const, if the underlying type is immutable.

I mean mutable, of course.

No, it's not. It's not allowed to mutate with inout. -Steve

Right, but inout can accept any of mutable, const, and mutable. The compiler will statically disallow you from mutating an inout variable, but if you know the underlying data is mutable, it's safe to cast inout away and mutate. If the data is actually mutable, then inout is effectively logical const. I guess the same is true of regular const.

inout does not know when the incoming data is mutable. Effectively, inout is just like const, but has implications on the return value. -Steve
Feb 15 2014
prev sibling next sibling parent "Meta" <jared771 gmail.com> writes:
On Sunday, 16 February 2014 at 02:41:36 UTC, Steven Schveighoffer 
wrote:
 inout does not know when the incoming data is mutable.

 Effectively, inout is just like const, but has implications on 
 the return value.

 -Steve

Yeah, but say you magically knew the data was mutable. void logicalConstBump(T)(ref inout(T) t, string originalMutability) { if (originalMutability == "mutable") { cast(T)t += 1; } else { assert(0); } } Obviously this is extremely contrived, but inout is effectively logical const here. Is there no way to know what the "original" mutability of t is at compile-time?
Feb 15 2014
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Sat, 15 Feb 2014 22:04:25 -0500, Meta <jared771 gmail.com> wrote:

 Obviously this is extremely contrived, but inout is effectively logical  
 const here. Is there no way to know what the "original" mutability of t  
 is at compile-time?

No, because only one version of the function is generated. That is part of the benefit of inout over a template. If you DO want to know whether it's mutable, use a template. -Steve
Feb 15 2014
prev sibling next sibling parent "Meta" <jared771 gmail.com> writes:
On Sunday, 16 February 2014 at 03:16:16 UTC, Steven Schveighoffer 
wrote:
 On Sat, 15 Feb 2014 22:04:25 -0500, Meta <jared771 gmail.com> 
 wrote:

 Obviously this is extremely contrived, but inout is 
 effectively logical const here. Is there no way to know what 
 the "original" mutability of t is at compile-time?

No, because only one version of the function is generated. That is part of the benefit of inout over a template. If you DO want to know whether it's mutable, use a template. -Steve

If you pass a mutable value to a function taking an inout parameter, what's the difference between that function and an identical function that takes a regular mutable parameter instead of an inout parameter?
Feb 15 2014
prev sibling next sibling parent "Stanislav Blinov" <stanislav.blinov gmail.com> writes:
On Saturday, 15 February 2014 at 04:03:51 UTC, Adam D. Ruppe 
wrote:

What about a library solution for something like C++-esque 
mutable?

struct Mutable(T)
{
     private T val;

     this(T v) { val = v; }

      property ref T get() const { return *cast(T*)&val; }
     alias get this;
}

import std.stdio;

struct Foo
{
     private Mutable!int cache;

     void bar() const
     {
         writeln("Updating cache");
         ++cache;
     }

      property int cached() const { return cache; }
}

void main()
{
     Foo foo;
     writeln(foo.cached);
     foo.bar();
     writeln(foo.cached);
}
Feb 15 2014
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Sat, 15 Feb 2014 22:17:48 -0500, Meta <jared771 gmail.com> wrote:

 On Sunday, 16 February 2014 at 03:16:16 UTC, Steven Schveighoffer wrote:
 On Sat, 15 Feb 2014 22:04:25 -0500, Meta <jared771 gmail.com> wrote:

 Obviously this is extremely contrived, but inout is effectively  
 logical const here. Is there no way to know what the "original"  
 mutability of t is at compile-time?

No, because only one version of the function is generated. That is part of the benefit of inout over a template. If you DO want to know whether it's mutable, use a template. -Steve

If you pass a mutable value to a function taking an inout parameter, what's the difference between that function and an identical function that takes a regular mutable parameter instead of an inout parameter?

I'm not sure what you are asking, because the answer I want to give is so trivial :) Identical functions produce identical code. Const, inout, immutable, whatever, these do not change code generation, just what is allowed to those variables. The rational behind inout came from the strstr problem: const char *strstr(const char *haystack, const char *needle); 1. You want the signature of strstr to guarantee it will not change it's parameters. 2. You do not want strstr to change the CALLER's permissions on the parameters, which it effectively does by applying const to the return value. I want the result of strstr to have the same restrictions as the parameter I pass in (mutable, const or immutable). The only way to do this was with a template. But a template would generate 3 separate versions of strstr with the exact same code in it. Maybe an enterprising compiler can elide the 2 superfluous versions, but there's another problem -- the mutable version would be able to change the parameter! We gave up on item 1. inout does this correctly, and it makes a world of difference in accessors of structs and classes. -Steve
Feb 15 2014
prev sibling next sibling parent "Meta" <jared771 gmail.com> writes:
On Sunday, 16 February 2014 at 03:26:42 UTC, Steven Schveighoffer 
wrote:
 I'm not sure what you are asking, because the answer I want to 
 give is so trivial :)

Then I don't know what we're arguing about. I'm saying that if you pass mutable data to an inout function, inout effectively acts as logical const within that function, as the compiler doesn't allow you to modify the inout function argument but it is safe to cast away inout. Do you agree or disagree with this?
Feb 15 2014
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Sat, 15 Feb 2014 23:35:52 -0500, Meta <jared771 gmail.com> wrote:

 On Sunday, 16 February 2014 at 03:26:42 UTC, Steven Schveighoffer wrote:
 I'm not sure what you are asking, because the answer I want to give is  
 so trivial :)

Then I don't know what we're arguing about. I'm saying that if you pass mutable data to an inout function, inout effectively acts as logical const within that function, as the compiler doesn't allow you to modify the inout function argument but it is safe to cast away inout. Do you agree or disagree with this?

It is safe if you can guarantee the input is ultimately mutable. But there is no way to enforce such a guarantee. It is on the caller to make sure that is true. I don't see why inout should be identified as any different from const in this respect. You could just as easily say the same thing about const. -Steve
Feb 15 2014
prev sibling next sibling parent "Meta" <jared771 gmail.com> writes:
On Sunday, 16 February 2014 at 04:46:34 UTC, Steven Schveighoffer 
wrote:
 It is safe if you can guarantee the input is ultimately 
 mutable. But there is no way to enforce such a guarantee. It is 
 on the caller to make sure that is true.

 I don't see why inout should be identified as any different 
 from const in this respect. You could just as easily say the 
 same thing about const.

 -Steve

Which is what I said in an earlier post. Wouldn't you be able to do this with a template somehow? template LogicalConst(T) if (isMutable!T) { alias LogicalConst = inout(T); } void bumpWithConstArg(T)(ref LogicalConst!T t) { //Compiler error if you try to modify t //t += 1; //Fine, cast it to mutable then modify //Nothing unsafe here because we verified t is mutable cast(T)t += 1; } void main() { immutable n = 0; //No IFTI match because n is immutable bumpWithConstArg(n); auto m = 0; //Okay, m is mutable bumpWithConstArg(m); } This doesn't actually work, but this is just off the top of my head.
Feb 15 2014
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Sun, 16 Feb 2014 00:14:07 -0500, Meta <jared771 gmail.com> wrote:

 On Sunday, 16 February 2014 at 04:46:34 UTC, Steven Schveighoffer wrote:
 It is safe if you can guarantee the input is ultimately mutable. But  
 there is no way to enforce such a guarantee. It is on the caller to  
 make sure that is true.

 I don't see why inout should be identified as any different from const  
 in this respect. You could just as easily say the same thing about  
 const.

 -Steve

Which is what I said in an earlier post. Wouldn't you be able to do this with a template somehow? template LogicalConst(T) if (isMutable!T) { alias LogicalConst = inout(T); } void bumpWithConstArg(T)(ref LogicalConst!T t) { //Compiler error if you try to modify t //t += 1; //Fine, cast it to mutable then modify //Nothing unsafe here because we verified t is mutable cast(T)t += 1; } void main() { immutable n = 0; //No IFTI match because n is immutable bumpWithConstArg(n); auto m = 0; //Okay, m is mutable bumpWithConstArg(m); } This doesn't actually work, but this is just off the top of my head.

I think you are making a wrong turn somewhere. This has exactly the same behavior: void bumpWithConstArg(T)(ref T t) { t += 1; } void main() { immutable n = 0; bumpWithConstArg(n); // fails to compile auto m = 0; bumpWithConstArg(n); } The problem you have to solve is this: void foo(const(int) t) { bumpWithConstArg(t); } void main() { immutable n = 0; foo(n); // needs to fail auto m = 0; foo(m); // needs to work. } -Steve
Feb 15 2014
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Sun, 16 Feb 2014 00:22:33 -0500, Steven Schveighoffer  
<schveiguy yahoo.com> wrote:

 The problem you have to solve is this:

 void foo(const(int) t)

Gahh... should have been: void foo(ref const(int) t) -Steve
Feb 15 2014
prev sibling next sibling parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Sunday, February 16, 2014 03:25:08 Stanislav Blinov wrote:
 On Saturday, 15 February 2014 at 04:03:51 UTC, Adam D. Ruppe
 wrote:
 
 What about a library solution for something like C++-esque
 mutable?

You're casting away const if you do that, and that's precisely what you shouldn't be doing. const is for _physical_ constness and should never be used if you want logical constness. Even attempting to use const for logical constness is incredibly dangerous, and the compiler is free to change what it does based on the knowledge that an object cannot be changed via a const reference and that an immutable object can never be changed. So, even if you manage to get away with casting away const and mutating in a particular situation right now, there's no guarantee that that code will work across compilers or architectures or that it will continue to work on future versions of the same compiler. The best thing to do is to just forget about even attempting to use const for logical constness. That's not what it's for, and you're going to have bugs (potentially very nasty and subtle bugs) if you attempt it. - Jonathan M Davis
Feb 15 2014
prev sibling next sibling parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Sunday, February 16, 2014 04:35:52 Meta wrote:
 On Sunday, 16 February 2014 at 03:26:42 UTC, Steven Schveighoffer
 
 wrote:
 I'm not sure what you are asking, because the answer I want to
 give is so trivial :)

Then I don't know what we're arguing about. I'm saying that if you pass mutable data to an inout function, inout effectively acts as logical const within that function, as the compiler doesn't allow you to modify the inout function argument but it is safe to cast away inout. Do you agree or disagree with this?

And that would be pointless. If a parameter is inout, then you have to treat it as const and cannot possibly know that the argument is mutable unless it's only ever passed a mutable argument and the programmer makes sure that that's all it's ever passed. And if that's the case, what's the point of having it be inout instead of mutable? And you really shouldn't be thinking of inout as being different from const from the function's perspective. The only difference is that if the caller passes a mutable or immutable argument, then they get a return value with the same mutability instead of it ending up as const like it would if the function took const instead of inout. It makes no difference to the function itself, and the function writer should not do anything differently inside the function than they would do if it took const. - Jonathan M Davis
Feb 15 2014
prev sibling next sibling parent "Stanislav Blinov" <stanislav.blinov gmail.com> writes:
On Sunday, 16 February 2014 at 06:28:45 UTC, Jonathan M Davis 
wrote:
 On Sunday, February 16, 2014 03:25:08 Stanislav Blinov wrote:
 On Saturday, 15 February 2014 at 04:03:51 UTC, Adam D. Ruppe
 wrote:
 
 What about a library solution for something like C++-esque
 mutable?

You're casting away const if you do that, and that's precisely what you shouldn't be doing. const is for _physical_ constness and should never be used if you want logical constness...

I hear you. In this case, one might ask why casting away const is allowed at all :)
Feb 15 2014
prev sibling next sibling parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Sunday, February 16, 2014 06:55:36 Stanislav Blinov wrote:
 On Sunday, 16 February 2014 at 06:28:45 UTC, Jonathan M Davis
 
 wrote:
 On Sunday, February 16, 2014 03:25:08 Stanislav Blinov wrote:
 On Saturday, 15 February 2014 at 04:03:51 UTC, Adam D. Ruppe
 wrote:
 
 What about a library solution for something like C++-esque
 mutable?

You're casting away const if you do that, and that's precisely what you shouldn't be doing. const is for _physical_ constness and should never be used if you want logical constness...

I hear you. In this case, one might ask why casting away const is allowed at all :)

Primarily for cases where you have to pass a const object to a function which has mutable parameters, and you know that it's not going to mutate its parameters - that and the fact that D is a systems language, so it will let you blow off your foot if you really try. It _is_ possible to mutate a const object without breaking code, but you have to know exactly what you're doing, and it's very, very risky such that it's pretty much always a bad idea. But if you really want to try blowing your foot off, D will let you. It just protects you such that you can't do it without trying (e.g. by casting away const). - Jonathan M Davis
Feb 16 2014
prev sibling parent "Dicebot" <public dicebot.lv> writes:
IMHO only practically usable "logical const" alternative in D are 
getters (in absence of setters). For all other cases better to 
simply abandon the concept, it simply does not fit the type 
system.
Feb 16 2014