www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - difficulties with const structs and alias this / template functions

reply Dennis <dkorpel gmail.com> writes:
I'm making a fixed point numeric type and want it to work 
correctly with const. First problem:

```
const q16 a = 6;
a /= 2;          // compiles! despite `a` being const.
writeln(a);      // still 6
a.toQ32 /= 2;    // what's actually happening
```

My q16 type has an implicit conversion to q32 (like how int can 
be converted to long):
```
q32 toQ32() const {
   return q32(...);
}
alias toQ32 this;
```
How do I make it so that a const(q16) will be converted to a 
const(q32) instead of mutable q32?

Second problem:
```
Q log2(Q)(Q num) if (is(Q : q16) || is(Q : q32)) {
     import std.traits: Unqual;
     Unqual!Q x = num;
     // actual code
}
```
When I call this with a const(q16), Q is resolved to const(q16) 
so I have to unqualify Q every time. It works, but feels clumsy. 
Is there an easier way to automatically de-const parameters? 
We're working with small value types here, it should be simple.

If anyone knows any other pitfalls with const, I'd love to know 
them.
Nov 18 2018
next sibling parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Sunday, 18 November 2018 at 17:30:18 UTC, Dennis wrote:
 I'm making a fixed point numeric type and want it to work 
 correctly with const. First problem:

 ```
 const q16 a = 6;
 a /= 2;          // compiles! despite `a` being const.
Ouch. That's actually kind of nasty.
 writeln(a);      // still 6
 a.toQ32 /= 2;    // what's actually happening
 ```

 My q16 type has an implicit conversion to q32 (like how int can 
 be converted to long):
 ```
 q32 toQ32() const {
   return q32(...);
 }
 alias toQ32 this;
 ```
 How do I make it so that a const(q16) will be converted to a 
 const(q32) instead of mutable q32?
Like this: // implement separate methods for mutable/const/immutable q32 toQ32() { return q32(x); } const(q32) toQ32() const { return q32(x); } immutable(q32) toQ32() immutable { return q32(x); } Or like this: // implement all three in one method, using the `this template` feature auto toQ32(this T)() { static if (is(T == immutable)) return immutable(q32)(x); else static if (is(T == const)) return const(q32)(x); else return q32(x); }
 Second problem:
 ```
 Q log2(Q)(Q num) if (is(Q : q16) || is(Q : q32)) {
     import std.traits: Unqual;
     Unqual!Q x = num;
     // actual code
 }
 ```
 When I call this with a const(q16), Q is resolved to const(q16) 
 so I have to unqualify Q every time. It works, but feels 
 clumsy. Is there an easier way to automatically de-const 
 parameters? We're working with small value types here, it 
 should be simple.
Define different overloads for Q and const Q. Or this: Q log2(Q)(inout Q num) if (is(Q : q16) || is(Q : q32)) { /* ... */ } Being able to jam mutable/const/immutable implementation in one function like that should tell you that you shouldn't mutate the argument. Then, the necessity to Unqual will go away on it's own ;)
Nov 18 2018
next sibling parent reply Dennis <dkorpel gmail.com> writes:
On Sunday, 18 November 2018 at 18:17:54 UTC, Stanislav Blinov 
wrote:
         // implement separate methods for 
 mutable/const/immutable
Thanks. I should have tried that, but I assumed it wouldn't work since you can't overload on return-type only. However, the const / non-const makes it allowed.
 Or like this:

         // implement all three in one method, using the `this 
 template` feature
That's new to me, interesting.
 Define different overloads for Q and const Q. Or this:

 Q log2(Q)(inout Q num) if (is(Q : q16) || is(Q : q32)) { /* ... 
 */ }

 Being able to jam mutable/const/immutable implementation in one 
 function like that should tell you that you shouldn't mutate 
 the argument. Then, the necessity to Unqual will go away on 
 it's own ;)
Different overloads sounds like a lot of boilerplate. inout still results in "cannot modify `inout` expression `input`" My goal is to be able to write straightforward and correct signatures for fixed point functions that receive mutable copies of whatever they are fed. This isn't even limited to my custom types: ``` T f0(T)(T x, T y) {return x += y;} int f1(int x, int y) {return x += y;} long f1(long x, long y) {return x += y;} void main() { import std.stdio; writeln(f0(const(int)(3), const(long)(4))); // can't modify const writeln(f1(const(int)(3), const(long)(4))); // fine } ``` The explicit overloads of f1 work fine, but the generic f0 does not. I could just use the f1 strategy of explicitly making overloads, but then adding a q64 or q128 type would mean having to double the existing boilerplate everywhere. I would like to have a `isFixedPoint` template and generic functions that take any isFixedPoint!Q, but it seems I have to manually enforce "don't forget to unqual Q first!".
Nov 18 2018
parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Sunday, 18 November 2018 at 20:10:52 UTC, Dennis wrote:
 On Sunday, 18 November 2018 at 18:17:54 UTC, Stanislav Blinov 
 wrote:
 Q log2(Q)(inout Q num) if (is(Q : q16) || is(Q : q32)) { /* 
 ... */ }

 Being able to jam mutable/const/immutable implementation in 
 one function like that should tell you that you shouldn't 
 mutate the argument. Then, the necessity to Unqual will go 
 away on it's own ;)
Different overloads sounds like a lot of boilerplate. inout still results in "cannot modify `inout` expression `input`"
You just dismissed that second to last sentence, did you? :)
 My goal is to be able to write straightforward and correct 
 signatures for fixed point functions that receive mutable 
 copies of whatever they are fed. This isn't even limited to my 
 custom types:

 ```
 T f0(T)(T x, T y) {return x += y;}
 int  f1(int  x, int  y) {return x += y;}
 long f1(long x, long y) {return x += y;}
 ```
 The explicit overloads of f1 work fine, but the generic f0 does 
 not.
``` T f0(T)(inout T x, inout T y) { return x + y; } ``` ;) But if you really really want to mutate the argument, then handling different mutability for T is the only way: ``` T f0(T)(T x, T y) { import std.traits : isMutable; static if (isMutable!T) return x += y; else return x + y; } ``` I know it's a bit painful though. In fact, Phobos also suffers from it. In std.numeric: T gcd(T)(T a, T b) if (isIntegral!T) { static if (is(T == const) || is(T == immutable)) { return gcd!(Unqual!T)(a, b); } // ... } Not only that looks ugly, but (with DMD) it makes gcd a doulbe function call :D
Nov 18 2018
parent reply Dennis <dkorpel gmail.com> writes:
On Monday, 19 November 2018 at 01:13:29 UTC, Stanislav Blinov 
wrote:
 You just dismissed that second to last sentence, did you? :)
I don't know what you mean with it. It's not that I'm trying to be sneaky or lazy really trying to modify the const parameter when I should treat it properly. And as the author I'm fine with Unqualing everything if that's needed, my concern is when another person using my type tries to write his own function: ``` q16 pow(q16 base, int exponent) { q16 result = 1; foreach(i; 0..exponent) result *= base; return result; } const q16 x = 3; writeln(pow(x, 3)); //works! ``` He then wants to make it more generic, so he rewrites: ``` Q pow(Q base, int exponent) if (isFixedPoint!Q) { Q result = 1; foreach(i; 0..exponent) result *= base; return result; } ``` And initially it seems to work, but as soon as it is used with const it breaks as `result` can't be mutated anymore. I'd like to set the example of writing proper generic functions, and if there is something simpler than importing Unqual I'd prefer that over my current solution. If there isn't, I'll just need to write a "don't forget to Unqual const" comment.
 ```
 T f0(T)(inout T x, inout T y) { return x + y; }
 ```

 ;)
What does inout do here? If the return type is also inout(T) I know that the return type gets the same qualifiers as inout parameters. But in this example, the return value is still T.
 I know it's a bit painful though. In fact, Phobos also suffers 
 from it. In std.numeric:
Yuck!
Nov 18 2018
parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Monday, 19 November 2018 at 02:03:18 UTC, Dennis wrote:
 On Monday, 19 November 2018 at 01:13:29 UTC, Stanislav Blinov 
 wrote:
 You just dismissed that second to last sentence, did you? :)
I don't know what you mean with it. It's not that I'm trying to be sneaky or lazy really trying to modify the const parameter when I should treat it properly. And as the author I'm fine with Unqualing everything if that's needed, my concern is when another person using my type tries to write his own function: ``` q16 pow(q16 base, int exponent) { q16 result = 1; foreach(i; 0..exponent) result *= base; return result; } const q16 x = 3; writeln(pow(x, 3)); //works! ``` He then wants to make it more generic, so he rewrites: ``` Q pow(Q base, int exponent) if (isFixedPoint!Q) { Q result = 1; foreach(i; 0..exponent) result *= base; return result; } ``` And initially it seems to work, but as soon as it is used with const it breaks as `result` can't be mutated anymore.
Yes, but that's not the problem with your type. It's a problem with the user not realizing that const is a type qualifier.
 I'd like to set the example of writing proper generic 
 functions, and if there is something simpler than importing 
 Unqual I'd prefer that over my current solution. If there 
 isn't, I'll just need to write a "don't forget to Unqual const" 
 comment.
 ```
 T f0(T)(inout T x, inout T y) { return x + y; }
 ```

 ;)
What does inout do here?
You're right, it's not needed there at all.
 If the return type is also inout(T) I know that the return type 
 gets the same qualifiers as inout parameters. But in this 
 example, the return value is still T.
Because, as before, value types are all copyable between mutable/const/immutable. So even if `return x + y` would yield a `const T`, you can still instantiate a T from it.
Nov 18 2018
prev sibling parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On 11/18/18 1:17 PM, Stanislav Blinov wrote:
 On Sunday, 18 November 2018 at 17:30:18 UTC, Dennis wrote:
 I'm making a fixed point numeric type and want it to work correctly 
 with const. First problem:

 ```
 const q16 a = 6;
 a /= 2;          // compiles! despite `a` being const.
Ouch. That's actually kind of nasty.
 writeln(a);      // still 6
 a.toQ32 /= 2;    // what's actually happening
 ```

 My q16 type has an implicit conversion to q32 (like how int can be 
 converted to long):
 ```
 q32 toQ32() const {
   return q32(...);
 }
 alias toQ32 this;
 ```
 How do I make it so that a const(q16) will be converted to a 
 const(q32) instead of mutable q32?
Like this:         // implement separate methods for mutable/const/immutable         q32 toQ32() {             return q32(x);         }         const(q32) toQ32() const {             return q32(x);         }         immutable(q32) toQ32() immutable {             return q32(x);         } Or like this:         // implement all three in one method, using the `this template` feature         auto toQ32(this T)() {             static if (is(T == immutable))                 return immutable(q32)(x);             else static if (is(T == const))                 return const(q32)(x);             else                 return q32(x);         }
Or just use inout. This is literally what inout is for: inout(q32) toQ32 inout { return q32(x); } This should transfer whatever constancy of the original is used for the return value. However, I'd state that this is really a workaround for a language deficiency. In reality, I would surmise (without knowing the implementation) that q32's state is a complete copy of the q16 state. So there is no reason to apply any constancy copying from the source to the destination. The real problem is that mutating operators on struct rvalues are always allowed, because `this` is always passed by reference. For the most part this is a harmless drawback, but because there is no way to "opt out" of this, you can't stop it when it really doesn't make sense (as in this case).
 Second problem:
 ```
 Q log2(Q)(Q num) if (is(Q : q16) || is(Q : q32)) {
     import std.traits: Unqual;
     Unqual!Q x = num;
     // actual code
 }
 ```
 When I call this with a const(q16), Q is resolved to const(q16) so I 
 have to unqualify Q every time. It works, but feels clumsy. Is there 
 an easier way to automatically de-const parameters? We're working with 
 small value types here, it should be simple.
Define different overloads for Q and const Q. Or this: Q log2(Q)(inout Q num) if (is(Q : q16) || is(Q : q32)) { /* ... */ } Being able to jam mutable/const/immutable implementation in one function like that should tell you that you shouldn't mutate the argument. Then, the necessity to Unqual will go away on it's own ;)
I have long wanted a way to direct IFTI how to define its parameters base on the arguments. We have a simple adjustment for arrays, where the array is always unqual'd before IFTI define the parameter. In other words: const int[] arr; void foo(T)(T t) {... } foo(arr) => T = const(int)[], not const(int[]) I think Andrei in the last dconf proposed we do more of this with other types (for ranges, specifically). But I think it would be good to also define other conversions possibly manually. -Steve
Nov 19 2018
parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Monday, 19 November 2018 at 14:51:14 UTC, Steven Schveighoffer 
wrote:

 Or just use inout. This is literally what inout is for:

 inout(q32) toQ32 inout {
     return q32(x);
 }

 This should transfer whatever constancy of the original is used 
 for the return value.
Yep, I just wanted to explicitly point out the `this T`, which gets overlooked way too often.
 However, I'd state that this is really a workaround for a 
 language deficiency. In reality, I would surmise (without 
 knowing the implementation) that q32's state is a complete copy 
 of the q16 state. So there is no reason to apply any constancy 
 copying from the source to the destination.
 The real problem is that mutating operators on struct rvalues 
 are always allowed, because `this` is always passed by 
 reference. For the most part this is a harmless drawback, but 
 because there is no way to "opt out" of this, you can't stop it 
 when it really doesn't make sense (as in this case).
Sure. At first I was perplexed why Dennis' a /= 2 even compiled. Then I saw the alias this.
 I have long wanted a way to direct IFTI how to define its 
 parameters base on the arguments. We have a simple adjustment 
 for arrays, where the array is always unqual'd before IFTI 
 define the parameter.

 In other words:

 const int[] arr;

 void foo(T)(T t) {... }

 foo(arr) => T = const(int)[], not const(int[])

 I think Andrei in the last dconf proposed we do more of this 
 with other types (for ranges, specifically). But I think it 
 would be good to also define other conversions possibly 
 manually.
I agree completely. Like Dennis' code, or that gcd example from Phobos, it'd really help to be able to be explicit in the signature, not creative in implementation :) Especially considering that Unqual is a high-yield type system nuke.
Nov 19 2018
prev sibling parent reply Rubn <where is.this> writes:
On Sunday, 18 November 2018 at 17:30:18 UTC, Dennis wrote:
 I'm making a fixed point numeric type and want it to work 
 correctly with const. First problem:

 ```
 const q16 a = 6;
 a /= 2;          // compiles! despite `a` being const.
 writeln(a);      // still 6
 a.toQ32 /= 2;    // what's actually happening
 ```

 My q16 type has an implicit conversion to q32 (like how int can 
 be converted to long):
 ```
 q32 toQ32() const {
   return q32(...);
 }
 alias toQ32 this;
 ```
 How do I make it so that a const(q16) will be converted to a 
 const(q32) instead of mutable q32?

 Second problem:
 ```
 Q log2(Q)(Q num) if (is(Q : q16) || is(Q : q32)) {
     import std.traits: Unqual;
     Unqual!Q x = num;
     // actual code
 }
 ```
 When I call this with a const(q16), Q is resolved to const(q16) 
 so I have to unqualify Q every time. It works, but feels 
 clumsy. Is there an easier way to automatically de-const 
 parameters? We're working with small value types here, it 
 should be simple.

 If anyone knows any other pitfalls with const, I'd love to know 
 them.
Yah most people tend to avoid const for this reason. It only really works for basic types, if you have a "const int" you can convert it to an "int" by copy. But if you have a type like Vector!(const int) that won't work, you can't even convert Vector!int to Vector!(const int) easily for example.
 ```
 Q log2(Q)(Q num) if (is(Q : q16) || is(Q : q32)) {
     import std.traits: Unqual;
     Unqual!Q x = num;
     // actual code
 }
 ```
This is pretty much the only way, you can just add alias V = Unqual!Q; then use V in your function instead of Unqual!Q everywhere.
Nov 18 2018
parent reply Dennis <dkorpel gmail.com> writes:
On Sunday, 18 November 2018 at 22:30:52 UTC, Rubn wrote:
 Yah most people tend to avoid const for this reason. It only 
 really works for basic types, if you have a "const int" you can 
 convert it to an "int" by copy. But if you have a type like 
 Vector!(const int) that won't work, you can't even convert 
 Vector!int to Vector!(const int) easily for example.
That's unfortunate. I can relate better to Jonathan's article [1] now. But I'll still try to make it working with const since other people might want to use it. I'm also trying to make it work with immutable, and from BigInt [2] I learned that constructors need to be `pure` for creating immutable objects. (I don't know why.) Are there any other gotchas? I didn't add an immutable variant for toQ32 like Stanislav suggested, but creating an immutable q32 from a q16 still seems to work fine. [1] http://jmdavisprog.com/articles/why-const-sucks.html [2] https://issues.dlang.org/show_bug.cgi?id=17330
Nov 18 2018
parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Monday, 19 November 2018 at 00:50:28 UTC, Dennis wrote:

 I'm also trying to make it work with immutable, and from BigInt 
 [2] I learned that constructors need to be `pure` for creating 
 immutable objects. (I don't know why.)
That's only for types with indirections (pointers), since `pure` guarantees that you do not mutate any global state. For value types, they work just fine: struct q16 { uint x; this(uint x) { this.x = x; } this(uint x) const { this.x = x; } this(uint x) immutable { this.x = x; } // or again, all three packed together: this(this T)(uint x) { this.x = x; } }
 Are there any other gotchas? I didn't add an immutable variant 
 for toQ32 like Stanislav suggested, but creating an immutable 
 q32 from a q16 still seems to work fine.
Yup, that's because, like Rubn said, copying value types is trivial. Where it all comes to bite you is when you start having pointers, because you can't copy a const(T)* into a T*.
Nov 18 2018
parent reply Dennis <dkorpel gmail.com> writes:
On Monday, 19 November 2018 at 01:24:02 UTC, Stanislav Blinov 
wrote:
 Yup, that's because, like Rubn said, copying value types is 
 trivial. Where it all comes to bite you is when you start 
 having pointers, because you can't copy a const(T)* into a T*.
I'm not using reference types, but still: ``` struct S { int a; this(int a) { this.a = a; } } void main() { immutable S d = 3; } ``` onlineapp.d(10): Error: mutable method onlineapp.S.this is not callable using a immutable object onlineapp.d(10): Consider adding const or inout to onlineapp.S.this const still leaves the first error, inout works though I don't know what it does. Adding pure also works.
Nov 18 2018
parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Monday, 19 November 2018 at 02:08:14 UTC, Dennis wrote:
 On Monday, 19 November 2018 at 01:24:02 UTC, Stanislav Blinov 
 wrote:
 Yup, that's because, like Rubn said, copying value types is 
 trivial. Where it all comes to bite you is when you start 
 having pointers, because you can't copy a const(T)* into a T*.
I'm not using reference types, but still: ``` struct S { int a; this(int a) { this.a = a; } } void main() { immutable S d = 3; } ``` onlineapp.d(10): Error: mutable method onlineapp.S.this is not callable using a immutable object onlineapp.d(10): Consider adding const or inout to onlineapp.S.this const still leaves the first error...
You're skimming the examples ;) struct S { int a; this(int a) { this.a = a; } this(int a) const { this.a = a; } this(int a) immutable { this.a = a; } } or struct S { int a; this(this T)(int a) { this.a = a; } }
 ...inout works though I don't know what it does.
Recall that member functions (including constructors) are just functions in disguise: struct S { this(int a) inout { /* ... */ } } what that boils down to, conceptually, is: void _compiler_made_it_a_struct_S_constructor(ref inout S this, int a); In other words, an `inout` method makes the "this" reference an `inout`. Same goes for const, immutable, etc.
Nov 18 2018
parent reply Dennis <dkorpel gmail.com> writes:
On Monday, 19 November 2018 at 02:39:32 UTC, Stanislav Blinov 
wrote:
 You're skimming the examples ;)
I'm not saying your examples don't work, I'm trying to understand the minimum requirements. You said: "That's [constructors needing to be pure is] only for types with indirections (pointers), since `pure` guarantees that you do not mutate any global state." My example was only to show that: - a const constructor is insufficient for creating an immutable struct S - an immutable or pure constructor is sufficient for creating an immutable struct S You also showed that an inout constructor is sufficient too. I don't understand why not any constructor would work. After all: "value types are all copyable between mutable/const/immutable. So even if `return x + y` would yield a `const T`, you can still instantiate a T from it."
 Recall that member functions (including constructors) are just 
 functions in disguise:
 ...
 what that boils down to, conceptually, is:
I get what inout does know. But continuing on the constructor lowering, what strikes me is that: S __ctor(ref const S this, int x) { this.x = x; // allowed, while `this` is a const(S)! // return this; automatically inserted } Knowing that a constructor may mutate the const(this) or immutable(this) members, why can't a mutable(this) constructor be called on an immutable(this), *unless* it is pure (which seems irrelevant to me)?
Nov 19 2018
parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Monday, 19 November 2018 at 12:28:43 UTC, Dennis wrote:
 On Monday, 19 November 2018 at 02:39:32 UTC, Stanislav Blinov 
 wrote:
 You're skimming the examples ;)
I'm not saying your examples don't work, I'm trying to understand the minimum requirements. You said: "That's [constructors needing to be pure is] only for types with indirections (pointers), since `pure` guarantees that you do not mutate any global state." My example was only to show that: - a const constructor is insufficient for creating an immutable struct S - an immutable or pure constructor is sufficient for creating an immutable struct S
Yes, that's what I meant.
 You also showed that an inout constructor is sufficient too.

 I don't understand why not any constructor would work. After 
 all:

 "value types are all copyable between mutable/const/immutable. 
 So even if `return x + y` would yield a `const T`, you can 
 still instantiate a T from it."
Because the rules still hold: this(int) -> __ctor(T, int); this(int) const -> __ctor(const T, int); this(int) immutable -> __ctor(immutable T, int);
 Recall that member functions (including constructors) are just 
 functions in disguise:
 ...
 what that boils down to, conceptually, is:
I get what inout does know. But continuing on the constructor lowering, what strikes me is that: S __ctor(ref const S this, int x) { this.x = x; // allowed, while `this` is a const(S)! // return this; automatically inserted } Knowing that a constructor may mutate the const(this) or immutable(this) members, why can't a mutable(this) constructor be called on an immutable(this), *unless* it is pure (which seems irrelevant to me)?
Because it's not mutation, it's initialization. Just like you write: const int x = 1; // initializes a const int x = 10; // error, can't mutate ...the first assignment to a member in a constructor is initialization: struct S { int x; this(int x) immutable { this.x = x; // ok, initialization this.x = 5; // error, can't mutate } } When a ctor is `pure`, the compiler knows it doesn't mutate any state other than the object's, so it allows conversion to all three qualifiers. What you can do, however, if you don't have an const/immutable constructor, is call a mutable constructor explicitly, so long as conversion is possible (i.e. value types): struct S { this(int) /* not immutable */ {} } immutable S x = 1; // error immutable S x = S(1); // ok
Nov 19 2018
parent reply Dennis <dkorpel gmail.com> writes:
On Monday, 19 November 2018 at 13:34:50 UTC, Stanislav Blinov 
wrote:
 Because it's not mutation, it's initialization.
Ohhhhh. That's an epiphany for me.
 When a ctor is `pure`, the compiler knows it doesn't mutate any 
 state other than the object's, so it allows conversion to all 
 three qualifiers.
I had trouble thinking of an example where impure constructors would break immutable, but now I get it: ``` int* gPtr; struct S { int a; this(int a) { this.a = a; gPtr = &this.a; } } void main() { S s = 1; (*gPtr)++; } ``` s can't be immutable because its field could be mutated, so the constructor needs to be pure (so gPtr can't be set) or immutable (so &this.a is an immutable(int)*).
 What you can do, however, if you don't have an const/immutable 
 constructor, is call a mutable constructor explicitly, so long 
 as conversion is possible (i.e. value types)
Interesting.
Nov 19 2018
parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Monday, 19 November 2018 at 14:04:29 UTC, Dennis wrote:
 On Monday, 19 November 2018 at 13:34:50 UTC, Stanislav Blinov 
 wrote:
 Because it's not mutation, it's initialization.
Ohhhhh. That's an epiphany for me.
:)
 When a ctor is `pure`, the compiler knows it doesn't mutate 
 any state other than the object's, so it allows conversion to 
 all three qualifiers.
I had trouble thinking of an example where impure constructors would break immutable, but now I get it: ``` int* gPtr; struct S { int a; this(int a) { this.a = a; gPtr = &this.a; } } void main() { S s = 1; (*gPtr)++; } ``` s can't be immutable because its field could be mutated, so the constructor needs to be pure (so gPtr can't be set) or immutable (so &this.a is an immutable(int)*).
Yep. Or the other way around (as I said earlier, it really makes sense when you have indirections inside the struct): int* global; struct S { const(int)* a; this(bool b) { if (b) a = global; else a = new int; } } You obviously can't instantiate an immutable S with that ctor, since const(int)* may point to mutable data. Nor can you make that ctor `pure`. However this is fine: immutable(int*) global; // note it's an immutable(int*), not an immutable(int)* struct S { const(int)* a; this(bool b) pure { if (b) a = global; else a = new int; } }
Nov 19 2018