www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - const and immutable members

reply "Daniel Davidson" <nospam spam.com> writes:
In this thread 
(http://forum.dlang.org/thread/uvhwkgljavskqfueqyxo forum.dlang.org) 
I asked this:

 3) Also, is storing immutable(STUFF) in a struct in the general
 case (as opposed to just this one) useful or silly?
Johnathan M Davis replied:
 As soon as you have a const or immutable member in a
 struct, you can't ever assign anything to it, even if
 all of its other members are mutable (you could
 assign the individual, mutable members but not the
 whole struct). That means stuff like if the struct is
 ever default-initialized, you can't even give it
 another value, which tends to mean that you can't use
 such a struct in places like arrays.

 All in all, I think that it's pretty much always a
 bad idea to have a struct with const or immutable
 members. It's just begging for problems. Tail-const
 or tail-immutable is fine, because the members
 themselves aren't const or immutable (just what they
 refer to), but having them be directly const or
 immutable is a bad idea IMHO.
I don't really understand the _tail-const_, _tail-immutable_ argument. Why is _tail-const_ fine but not _const_ when there is transitivity anyway? I think there are only a handful of places you consider using const/immutable: - Global variables - Local variables - Member variables - Function signatures Are there any missing? If right out of the gate the feeling is member variables should not be const or immutable, doesn't that greatly reduce the value of the mutability specificiers? Members are used to hold onto the data for the lifecycle of the object and if those should not make claims or guarantees on mutability then that seems a shame. What do others think? Are there cases where const/immutable members are being used in a good way? In that thread I gave a motivation for it (I used emacs and the formatting turned out awful - so sorry about that). But not sure if my logic holds water. Thanks Dan
Sep 22 2013
next sibling parent reply Jonathan M Davis <jmdavisProg gmx.com> writes:
On Sunday, September 22, 2013 18:15:08 Daniel Davidson wrote:
 In this thread
 (http://forum.dlang.org/thread/uvhwkgljavskqfueqyxo forum.dlang.org)
 
 I asked this:
 3) Also, is storing immutable(STUFF) in a struct in the general
 case (as opposed to just this one) useful or silly?
Johnathan M Davis replied:
 As soon as you have a const or immutable member in a
 struct, you can't ever assign anything to it, even if
 all of its other members are mutable (you could
 assign the individual, mutable members but not the
 whole struct). That means stuff like if the struct is
 ever default-initialized, you can't even give it
 another value, which tends to mean that you can't use
 such a struct in places like arrays.
 
 All in all, I think that it's pretty much always a
 bad idea to have a struct with const or immutable
 members. It's just begging for problems. Tail-const
 or tail-immutable is fine, because the members
 themselves aren't const or immutable (just what they
 refer to), but having them be directly const or
 immutable is a bad idea IMHO.
I don't really understand the _tail-const_, _tail-immutable_ argument. Why is _tail-const_ fine but not _const_ when there is transitivity anyway?
If you have struct S { immutable int[] arr; } then arr can never be assigned to, so a variable of type S can never be assigned to. But if you have struct S { immutable(int)[] arr; } then arr can be reassigned as much as you'd like, so S can be assigned to. In both cases, the elements of the array are immutable, so it can freely be a slice of another array and not care or affect that other array, but the array variable itself - which only exists local to the struct - is not restricted in being assigned to. So, you get the immutability of the data without restricting the struct. All making arr itself immutable does (rather than tail-immutable) is make it so that you can't reassign arr, which can be useful sometimes, but in the case of a struct, it makes it so that the whole struct can't be reassigned, so it's pretty much never a good idea IMHO to have a struct with const or immutable members - but having them be tail-const or tail-immutable still makes it so that what they refer to gets all of the benefits of const or immutable without restricting the struct. If you're dealing with a class, then the situation is a bit different, because the class is always on the heap, and you don't normally assign to classes. You reassign the reference that refers to them or you assign to their member variables, but you don't assign to the block of data that is the class itself like you would with a struct. So, making a class' member variable const or immutable does not restrict the class, which means that if you want to make it so that the member variable can't be reassigned, making it fully const or fully immutable is fine.
 I think there are only a handful of places you consider using
 const/immutable:
 
 - Global variables
 - Local variables
 - Member variables
 - Function signatures
 
 Are there any missing?
You can use const and immutable anywhere that you declare a variable or member function.
 If right out of the gate the feeling is member variables should
 not be const or immutable, doesn't that greatly reduce the value
 of the mutability specificiers? Members are used to hold onto the
 data for the lifecycle of the object and if those should not make
 claims or guarantees on mutability then that seems a shame.
It's only a problem to make members fully const or immutable with structs, because then you can't reassign the struct, which does nasty things like make it so that you can't put them in arrays unless you want all the elements of the array to have the init value for that struct or you want to append each element individually (which won't work with static arrays). Also, the benefits of sharing data are only gained when that data is on the heap, in which case you can just make the data const or immutable without making the member variable in the struct const or immutable - you make it tail-const or tail-immutable rather than const or immutable. If the data were directly in the struct (i.e. the member variable is a value type), then the only way to share it would be via a pointer or via ref. ref doesn't really matter, since you can make that const, and it only refers to the data until the function call has completed. And pointers don't matter, because you can't actually rely on a pointer to a struct's member variable staying valid longer than ref would have anyway, because structs can be moved rather than copied. So, sharing via the heap is the only viable way, and if you do that, you can use tail-const or tail-immutable for all of the same benefits that making the member variable fully const or immutable would have given you (since you're sharing the data, not the variable). And again, the problems with making a member variable fully const or immutable lie with structs, because they sit directly on the stack or directly in an array, whereas classes sit on the heap and are only referred to via a reference. - Jonathan M Davis
Sep 22 2013
parent reply "Daniel Davidson" <nospam spam.com> writes:
On Sunday, 22 September 2013 at 20:17:03 UTC, Jonathan M Davis 
wrote:
 If you have

 struct S
 {
     immutable int[] arr;
 }

 then arr can never be assigned to, so a variable of type S can 
 never be
 assigned to. But if you have
Yes - it (arr) can never be assigned to. That is the idea. It has already been assigned to when the data was read in, prior to construction of S. arr is initialized in the ctor of S.
 struct S
 {
     immutable(int)[] arr;
 }

 then arr can be reassigned as much as you'd like, so S can be 
 assigned to.
I don't imagine wanting that in this case. I want to ensure the array passed at construction in is the same array being used in read-only fashion throughout the lifecycle of S. That is my intention behind using immutable. I'm saying I'll not change it and no one else can, other wise functions of S that might have made assumptions about arr could be invalidated.
 In
 both cases, the elements of the array are immutable, so it can 
 freely be a
 slice of another array and not care or affect that other array, 
 but the array
 variable itself - which only exists local to the struct - is 
 not restricted in
 being assigned to. So, you get the immutability of the data 
 without
 restricting the struct. All making arr itself immutable does 
 (rather than
 tail-immutable) is make it so that you can't reassign arr,
It also keeps the contents of arr from growing and surprising S . You could try to prevent with private, but still if it is a large module why not have the commitment not to change the contents (including the length), especially if it is important that you know the data is not changing.
 which can be useful
 sometimes, but in the case of a struct, it makes it so that the 
 whole struct
 can't be reassigned, so it's pretty much never a good idea IMHO 
 to have a
 struct with const or immutable members -
Why that conclusion? That is, why should reassignment of S take priority over stable data used by S? For example, take something like a BalanceSheetForecaster which takes a BalanceSheet as input in its ctor. Isn't it important that the numerous queries on and forecasts by the BalanceSheetForecaster be on *exactly* the same data?
 but having them be tail-const or
 tail-immutable still makes it so that what they refer to gets 
 all of the
 benefits of const or immutable without restricting the struct.
I appreciate the explanation and think I understand the benefit of tail-const better. You would not easily be able to store modifiable collections of BalanceSheetForecasters, unless you did it with pointers. But the focus of the discussion seems to be on immutable(T)[] which is my fault for choosing the first and easiest type of aliasing I could in my example. I think the slice is really a special case when it comes to aliasing. With immutable(T)[] you have the benefit of not having to worry about aliasing because of the way it is implemented - the contiguous nature and copy on write semantics. But this is not the general case is it? For example, do the same arguments hold for associative arrays? struct S { immutable string[string] aarr; } Doesn't using immutable there present the same problem as with the slice? S is no longer assignable. But who would recommend not using immutable in this case if you want aarr to be stable. If you do not use immutable then who knows when your array will grow without your expecting it? At least with the slice your memory safety is in the control of your module if you make it private. But with associative array it would depend entirely on client code providing the data still having a handle on it.
Sep 22 2013
parent reply Jonathan M Davis <jmdavisProg gmx.com> writes:
On Monday, September 23, 2013 05:18:31 Daniel Davidson wrote:
 On Sunday, 22 September 2013 at 20:17:03 UTC, Jonathan M Davis
 which can be useful
 sometimes, but in the case of a struct, it makes it so that the
 whole struct
 can't be reassigned, so it's pretty much never a good idea IMHO
 to have a
 struct with const or immutable members -
Why that conclusion? That is, why should reassignment of S take priority over stable data used by S? For example, take something like a BalanceSheetForecaster which takes a BalanceSheet as input in its ctor. Isn't it important that the numerous queries on and forecasts by the BalanceSheetForecaster be on *exactly* the same data?
If any of a struct's members are const, you reassign them in a postblit constructor, meaning that if they're reference types, you can't copy them. Dynamic arrays of S become almost useless (and static arrays _definitely become useless), because once any value of type S is initialized it can't be assigned. So, if you have S[12] foo; or auto bar = new S[](17); you can't reassign any of the elements in those arrays. Best case, you can mutate their members that aren't const or immutable, but all of their const and immutable members are stuck referring to the value in S.init. Anywhere that involves S.init stops working properly, because you can't change any S's that were default-initialized with S.init to something else. So, outside of the most basic cases where you just put an S on the stack by itself and don't pass it to anything or put it in an array or anything like that, you start running into trouble, because you can't change the value of S. Constrast that with where none of the member variables are const or immutable (or at most are tail-const or tail-immutable), and you can only not assign S when it's a const S or immutable S. In that sort of situation, you can often create an S as mutable and then cast it to const or immutable and get around the problems with S.init. e.g. auto temp = new S[](3); temp[0] = baz(); temp[1] = S(12); temp[2] = blah(); auto bar = assumeUnique(temp); bar and all of its elmeents are now immutable, but you could actually tweak them initially, whereas if any of the member variables were actually fully const or immutable, you couldn't change any of them from their init value.
 but having them be tail-const or
 tail-immutable still makes it so that what they refer to gets
 all of the
 benefits of const or immutable without restricting the struct.
I appreciate the explanation and think I understand the benefit of tail-const better. You would not easily be able to store modifiable collections of BalanceSheetForecasters, unless you did it with pointers. But the focus of the discussion seems to be on immutable(T)[] which is my fault for choosing the first and easiest type of aliasing I could in my example. I think the slice is really a special case when it comes to aliasing. With immutable(T)[] you have the benefit of not having to worry about aliasing because of the way it is implemented - the contiguous nature and copy on write semantics. But this is not the general case is it? For example, do the same arguments hold for associative arrays? struct S { immutable string[string] aarr; } Doesn't using immutable there present the same problem as with the slice? S is no longer assignable. But who would recommend not using immutable in this case if you want aarr to be stable. If you do not use immutable then who knows when your array will grow without your expecting it? At least with the slice your memory safety is in the control of your module if you make it private. But with associative array it would depend entirely on client code providing the data still having a handle on it.
Of course, it's the same for AAs. Or classes. Or pointers. Or int. Or float. It's the same for _any_ type. If you make any of them fully immutable, then you can't reassign the struct, which makes the struct unusable in a number of situations. But if your concern is client code messing with your member variable, then don't give them access to it in the first place. Encapsulate it properly, and use property functions or getters and setters to access it. If all you provide is a getter property or getter function and no setters, then they can't assign to it. And if it's a reference type, all you have to do is return a const reference to it rather than a mutable one, and client code won't be able to alter it. And in most cases, using tail-const also solves the problem, because then the reference can be altered but not the data. AAs are one of the few cases where that's not true, because there really isn't a way to have a tail- const AA (not without an AA equivalent to std.typecons.Rebindable anyway). But it works with pointers and arrays just fine, and it works with classes if you use std.typecons.Rebindable. Just return a mutable reference to const or immutable data, and you don't have to worry about client code mutating your data. If you're making your member variables const or immutable so that client code can't mutate them, then you're just causing yourself problems because you didn't use proper encapsulation. - Jonathan M Davis
Sep 22 2013
parent reply "Daniel Davidson" <nospam spam.com> writes:
On Monday, 23 September 2013 at 03:51:41 UTC, Jonathan M Davis 
wrote:
 Doesn't using immutable there present the same problem as with
 the slice? S is no longer assignable. But who would recommend 
 not
 using immutable in this case if you want aarr to be stable. If
 you do not use immutable then who knows when your array will 
 grow
 without your expecting it? At least with the slice your memory
 safety is in the control of your module if you make it private.
 But with associative array it would depend entirely on client
 code providing the data still having a handle on it.
Of course, it's the same for AAs. Or classes. Or pointers. Or int. Or float. It's the same for _any_ type. If you make any of them fully immutable, then you can't reassign the struct, which makes the struct unusable in a number of situations. But if your concern is client code messing with your member variable, then don't give them access to it in the first place.
Not quite as much messing with the member as messing with what it points to. In the setup - rich data, read from the wire or disk, lots of nesting of lists, associative arrays, json like data, etc, the POD data is being read and stored into nested structs all prior to coming to my class. I want to ensure that the memory that I know both of us (S and the client code who built data to pass to S and other users) is not being modified. I think of it like: int, double, char[10], immutable(T)[] (and string) are all similar in that there is no troublesome aliasing (even though there may be sharing). Associative array is different in that string[string] has troubling aliasing and your data can change from under you - unless you copy it deeply. It seems the suggestion from above would be copy in postblits. But I don't think that scales from a nested class perspective. The problem is where and how to copy. Actually, that was the approach I was taking before, copying the data up front in postblits. In fact I created a generalized dup function that given type T will recursively copy all fields, giving care to deep copy those with potential for mutable aliasing. The general comments in one of the related threads then was it was silly to copy the data up front. Walter in fact said he would avoid all copying data on struct construction, preferring lazy copy on write semantics. Walter's comment in this thread http://forum.dlang.org/thread/k8ti2b$1iqj$1 digitalmars.com?page=7 Have you ever written a struct that requires a deep copy? I have, and I always wound up redoing it so the deep copy was unnecessary. I believe I asked how that would be done in the general sense but I don't think there were any good mechanisms. By, "in the general sense" I'm talking about dealing with composition at maybe 5 to 7 layers and each struct containing types with mutable aliasing with lots of associative arrays.
 If you're making your member variables const or immutable so 
 that client code
 can't mutate them, then you're just causing yourself problems 
 because you
 didn't use proper encapsulation.
I'm now *considering* making them const because I don't want a large composition hierarchy of plain old data with postblits aggressively copying data unnecessarily before it gets finalized and passed to my struct S that is really only holding onto it as reference data/read only/forever immutable.
Sep 23 2013
parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Monday, September 23, 2013 13:49:59 Daniel Davidson wrote:
 But if your concern is client code messing with your member
 variable, then
 don't give them access to it in the first place.
Not quite as much messing with the member as messing with what it points to. In the setup - rich data, read from the wire or disk, lots of nesting of lists, associative arrays, json like data, etc, the POD data is being read and stored into nested structs all prior to coming to my class. I want to ensure that the memory that I know both of us (S and the client code who built data to pass to S and other users) is not being modified.
Then your member variable is going to be a reference type, and you can make it tail-const - except in the case of AAs, but if you simply return a const AA from a getter, then client code can't change it. In order to actually have tail-const AAs, we'd need something like Rebindable but for AAs (or maybe Rebindable could be made to work with AAs). But in general, if you want to share data and want to avoid copying, making the data is a good move, but you do it by making the data immutable and not what refers to it (i.e. tail- immutable). Then the data itself can be safely shared, but you don't run into all of the problems that come when you make a member variable in a struct fully const or immutable. - Jonathan M Davis
Sep 23 2013
prev sibling parent "anonymous" <anonymous example.com> writes:
On Sunday, 22 September 2013 at 16:15:09 UTC, Daniel Davidson 
wrote:
[...]
 3) Also, is storing immutable(STUFF) in a struct in the 
 general
 case (as opposed to just this one) useful or silly?
[...]
 I don't really understand the _tail-const_, _tail-immutable_ 
 argument. Why is _tail-const_ fine but not _const_ when there 
 is transitivity anyway?
[...]
 If right out of the gate the feeling is member variables should 
 not be const or immutable, doesn't that greatly reduce the 
 value of the mutability specificiers? Members are used to hold 
 onto the data for the lifecycle of the object and if those 
 should not make claims or guarantees on mutability then that 
 seems a shame.
By marking the tail of the member const -- e.g. const(int)[] --, you're committing to not altering the refered-to data. This means, the user of your struct can put in mutable and immutable data. When you mark it full-const -- const(int[]) -- you're taking away from the user the ability to make it refer to some other data. In particular, setting a variable of your struct type to a new value requires mutable members. There may be special cases where that extra restriction is desired, but I can't think of one. Unless you're in such a special case, it just hurts the user for no reason. And when the user needs a const object, they can do that on their own: struct S {int x;} const s = S(42); static assert(is(typeof(s.x) == const));
Sep 22 2013