www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Allow this() { } for structs

reply Q. Schroll <qs.il.paperinik gmail.com> writes:
In metaprogramming, I find myself writing stuff like

     struct S(Ts...)
     {
         this(Ts args)
         {
             static foreach (T; Ts) { /*...*/ }
         }
     }

and get compiler errors for the case that `Ts` is empty. So I 
need an actually useless

     static if (Ts.length != 0)
     this(Ts args) { /*...*/ }

Can we just allow this() when the body is essentially empty (that 
is: after rewrites and lowerings contains no statements)? The 
compiler would happily accept the code and pretend this() { } 
isn't there. It would even reduce confusion because S() is legal 
as an expression, but this() isn't as a constructor. With that, 
this() is legal as a constructor, but it must not do anything.
Nov 02 2020
parent reply FeepingCreature <feepingcreature gmail.com> writes:
On Monday, 2 November 2020 at 19:53:31 UTC, Q. Schroll wrote:
 Can we just allow this() when the body is essentially empty 
 (that is: after rewrites and lowerings contains no statements)? 
 The compiler would happily accept the code and pretend this() { 
 } isn't there. It would even reduce confusion because S() is 
 legal as an expression, but this() isn't as a constructor. With 
 that, this() is legal as a constructor, but it must not do 
 anything.
I just want this() always, even with a non-empty body. We run into an issue where we need predictable destructor calls for a struct constructed for a with(), but we also want possibly immutable data in the struct, so we *have* to use this() rather than static opCall() which is the standard workaround. So we end up with code like this: struct StructType { this(int) { ... } } StructType structType() { return StructType(0); } I do not understand what purpose the lack of this() serves. I mean, I understand what purpose it serves, it's so T.init is always equivalent to T(), but I don't understand what purpose *that* serves. T.init is *already* not a valid constructed T if you want to use any invariants and/or call any methods at all. We *already* can't treat them equivalently regardless, so from my perspective the ban on T() serves no purpose.
Nov 06 2020
next sibling parent IGotD- <nise nise.com> writes:
On Friday, 6 November 2020 at 09:54:23 UTC, FeepingCreature wrote:
 On Monday, 2 November 2020 at 19:53:31 UTC, Q. Schroll wrote:
 Can we just allow this() when the body is essentially empty 
 (that is: after rewrites and lowerings contains no 
 statements)? The compiler would happily accept the code and 
 pretend this() { } isn't there. It would even reduce confusion 
 because S() is legal as an expression, but this() isn't as a 
 constructor. With that, this() is legal as a constructor, but 
 it must not do anything.
I just want this() always, even with a non-empty body. We run into an issue where we need predictable destructor calls for a struct constructed for a with(), but we also want possibly immutable data in the struct, so we *have* to use this() rather than static opCall() which is the standard workaround. So we end up with code like this: struct StructType { this(int) { ... } } StructType structType() { return StructType(0); } I do not understand what purpose the lack of this() serves. I mean, I understand what purpose it serves, it's so T.init is always equivalent to T(), but I don't understand what purpose *that* serves. T.init is *already* not a valid constructed T if you want to use any invariants and/or call any methods at all. We *already* can't treat them equivalently regardless, so from my perspective the ban on T() serves no purpose.
It's been discussed before and it has been rejected, because some motivation based on compiler internals rather than the convenience of the programmer. The whole point of a computer language is that it is supposed to convenient for humans and not the other way around. Also, I don't see the point why we couldn't hack in support for a constructor without any arguments. We already have support for a constructor with arguments so I don't understand why without any would be impossible to implement. It's is highly inconvenient and instead I have to use an additional init() method with no argument but when I have arguments I can use the constructor directly. This really makes the language inconsistent.
Nov 06 2020
prev sibling parent reply Q. Schroll <qs.il.paperinik gmail.com> writes:
On Friday, 6 November 2020 at 09:54:23 UTC, FeepingCreature wrote:
 I do not understand what purpose the lack of this() serves. I 
 mean, I understand what purpose it serves, it's so T.init is 
 always equivalent to T(), but I don't understand what purpose 
 *that* serves. T.init is *already* not a valid constructed T if 
 you want to use any invariants and/or call any methods at all. 
 We *already* can't treat them equivalently regardless, so from 
 my perspective the ban on T() serves no purpose.
I understand the reasons. Type.init is nice to have for all types, but, unfortunately, life isn't fair and Type.init is garbage for some types, not only those with invariants. One could argue---and I'm leaning towards that side at least somewhat---that an invalid Type.init is worse than Type.init not existing for all types. If a generic algorithm won't work without Type.init then it won't work with all types. Generic algorithms not working with all types, but only those that provide specific things, is nothing new. In my opinion, if for a struct T, T() isn't disabled and defined, if it is pure, the compiler can reasonably say: enum init = T(); If T's this() isn't pure, T.init cannot exist. Use cases where this() wouldn't be pure are probably rare anyway. As a first step, the compiler could disallow impure this() at all. That being said, debug{} could be used to do something at construction in an impure manner.
Nov 06 2020
next sibling parent reply IGotD- <nise nise.com> writes:
On Friday, 6 November 2020 at 16:53:32 UTC, Q. Schroll wrote:
 If T's this() isn't pure, T.init cannot exist.
Why must T.init and this() be the same thing?
Nov 06 2020
next sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Friday, 6 November 2020 at 17:14:27 UTC, IGotD- wrote:
 On Friday, 6 November 2020 at 16:53:32 UTC, Q. Schroll wrote:
 If T's this() isn't pure, T.init cannot exist.
Why must T.init and this() be the same thing?
There is no point in having init if you have default constructors, then init would just be an optimization.
Nov 06 2020
prev sibling parent reply Q. Schroll <qs.il.paperinik gmail.com> writes:
On Friday, 6 November 2020 at 17:14:27 UTC, IGotD- wrote:
 On Friday, 6 November 2020 at 16:53:32 UTC, Q. Schroll wrote:
 If T's this() isn't pure, T.init cannot exist.
Why must T.init and this() be the same thing?
The more I think about T.init the more stupid I tend to find it. The best example is struct S { Object obj = new Object(); } not working as most people expect. There's *one* Object allocated, that S.init.obj refers to, and that one Object's reference is blitted into every constructed S. One way to get different immutable(S) with their own distinct obj is a pure S factory returning unique S so that it can be implicitly typed immutable; that factory can be static opCall. This is stupid; one shouldn't need factories for that. (Factories have their use cases, though.) The only other option is an immutable constructor (or a constructor suitable for constructing immutable), but since being a constructor, it needs a parameter. This is stupid; one shouldn't need to add useless parameters to any function, except maybe for compatibility with an interface. This isn't near that. They need not necessarily. I find, it would add unnecessary confusion if T.init isn't derived from a this() call. That way, at least it is guaranteed that T.init has T's invariants met at least locally. (Locally meaning that looking at the object isolated, no invariant violation can be derived.) An example for a "global" invariant is "no two objects of that type have the same id", something that clearly cannot be verified nor refuted looking at objects individually.
Nov 06 2020
parent reply IGotD- <nise nise.com> writes:
On Saturday, 7 November 2020 at 01:39:30 UTC, Q. Schroll wrote:
 This is stupid; one shouldn't need to add useless parameters to 
 any function, except maybe for compatibility with an interface. 
 This isn't near that.
Speaking of stupid, what the compiler can do is lowering the this() to something else. struct T { this(); } is lowered to struct T { this(StupidCompilerMadeUpType); } Then it just works, basically using your workaround inside the compiler. Horrible, but it would work.
Nov 07 2020
parent reply FeepingCreature <feepingcreature gmail.com> writes:
On Saturday, 7 November 2020 at 10:56:29 UTC, IGotD- wrote:
 On Saturday, 7 November 2020 at 01:39:30 UTC, Q. Schroll wrote:
 This is stupid; one shouldn't need to add useless parameters 
 to any function, except maybe for compatibility with an 
 interface. This isn't near that.
Speaking of stupid, what the compiler can do is lowering the this() to something else.
It's not like it doesn't work because the compiler devs couldn't figure out how to do it, it's that it clashes with the notion that all data types should have a valid default value. (As I understand it.) My point with the Nullable thing is that this notion is dead regardless.
Nov 07 2020
parent reply IGotD- <nise nise.com> writes:
On Saturday, 7 November 2020 at 15:00:51 UTC, FeepingCreature 
wrote:
 It's not like it doesn't work because the compiler devs 
 couldn't figure out how to do it, it's that it clashes with the 
 notion that all data types should have a valid default value. 
 (As I understand it.) My point with the Nullable thing is that 
 this notion is dead regardless.
I don't understand how this clashes with S.init. You can have a S.init and this() which are separate. First always initializing to S.init and then run the constructor if it exists. The optimizer merges these two steps when it is removing unused values. this() of course becomes a runtime initialization unless the optimizer figures it out.
Nov 07 2020
parent Paul Backus <snarwin gmail.com> writes:
On Saturday, 7 November 2020 at 15:18:27 UTC, IGotD- wrote:
 I don't understand how this clashes with S.init. You can have a 
 S.init and this() which are separate. First always initializing 
 to S.init and then run the constructor if it exists. The 
 optimizer merges these two steps when it is removing unused 
 values. this() of course becomes a runtime initialization 
 unless the optimizer figures it out.
Worth noting that this is already how things work for nested types. T.init is a compile-time constant and has its context pointer set to null, while T() initializes the context pointer at runtime.
Nov 07 2020
prev sibling parent FeepingCreature <feepingcreature gmail.com> writes:
On Friday, 6 November 2020 at 16:53:32 UTC, Q. Schroll wrote:
 On Friday, 6 November 2020 at 09:54:23 UTC, FeepingCreature 
 wrote:
 I do not understand what purpose the lack of this() serves. I 
 mean, I understand what purpose it serves, it's so T.init is 
 always equivalent to T(), but I don't understand what purpose 
 *that* serves. T.init is *already* not a valid constructed T 
 if you want to use any invariants and/or call any methods at 
 all. We *already* can't treat them equivalently regardless, so 
 from my perspective the ban on T() serves no purpose.
I understand the reasons. Type.init is nice to have for all types, but, unfortunately, life isn't fair and Type.init is garbage for some types, not only those with invariants. One could argue---and I'm leaning towards that side at least somewhat---that an invalid Type.init is worse than Type.init not existing for all types. If a generic algorithm won't work without Type.init then it won't work with all types. Generic algorithms not working with all types, but only those that provide specific things, is nothing new.
I used to think that T.init was required for really basic stuff like Nullable and sumtype, but that kind of data type *already* doesn't work with T.init because if it's unset, it needs to bypass the broken T.init destructor call. So we need to use the union hack there anyways, as I had to do for Nullable, and do a semi-complete endrun around the compiler's lifetime tracking regardless. That's why I think the importance of T.init is overstated nowadays, and adding T() probably wouldn't break anything fundamental.
Nov 07 2020