www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Struct initialization is a mess

reply Dukc <ajieskola gmail.com> writes:
In the `main` function, I have commented out the initialization 
forms that don't compile. Everything else compiles.

```d
struct Basic
{ int cont;
}

struct NoDef
{ int cont;
    disable this();
}

struct FalseInit
{ int cont;
   static float init;
}

struct Ctor
{ int cont;
   this(int){}
}

struct EmptyOpcall
{ int cont;
   static typeof(this) opCall(){return this.init;}
}

struct ArgumentedOpcall
{ int cont;
   static typeof(this) opCall(int){return this.init;}
}

void main()
{ {Basic a, b = Basic.init, c = Basic(), d = {}, e = Basic(0), f 
= {0};}
   {NoDef /*a,*/ b = NoDef.init, /*c = NoDef(), d = {}, e = 
NoDef(0),*/ f = {0};}
   {FalseInit a, /*b = FalseInit.init,*/ c = FalseInit(), d = {}, 
e = FalseInit(0), f = {0};}
   {Ctor a, b = Ctor.init, c = Ctor(),/*, d = {}*/ e = Ctor(0)/*, 
f = {0}*/;}
   {EmptyOpcall a, b = EmptyOpcall.init, c = EmptyOpcall(), d = 
{}, /*e = EmptyOpcall(0)*/ f = {0};}
   {ArgumentedOpcall a, b = ArgumentedOpcall.init, /*c = 
ArgumentedOpcall(),*/ d = {}, e = ArgumentedOpcall(0), f = {0};}
}
```

We have terribly many ways to initialize a struct (or union or 
class for that matter). I have trouble seeing the logic between 
all these.

Now granted, many of these make sense. Obviously, non-explicit 
initialization with ` disabled this()` is not supposed to 
compile. And I'm being unfair with the `FalseInit` example, 
defining that is just plain bad programming.

But still. Why `NoDef.init` compiles? Why C-style initialization 
is okay with `opCall`ed `struct`s but not with ones that have 
constructors? Why `Ctor()` is okay but `ArgumentedOpcall()` is 
not?

If there is some big picture, I am failing to see it.
Jul 28 2021
parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Wed, Jul 28, 2021 at 11:08:34PM +0000, Dukc via Digitalmars-d wrote:
[...]
 void main()
 { {Basic a, b = Basic.init, c = Basic(), d = {}, e = Basic(0), f = {0};}
   {NoDef /*a,*/ b = NoDef.init, /*c = NoDef(), d = {}, e = NoDef(0),*/ f =
 {0};}
   {FalseInit a, /*b = FalseInit.init,*/ c = FalseInit(), d = {}, e =
 FalseInit(0), f = {0};}
   {Ctor a, b = Ctor.init, c = Ctor(),/*, d = {}*/ e = Ctor(0)/*, f = {0}*/;}
   {EmptyOpcall a, b = EmptyOpcall.init, c = EmptyOpcall(), d = {}, /*e =
 EmptyOpcall(0)*/ f = {0};}
   {ArgumentedOpcall a, b = ArgumentedOpcall.init, /*c =
 ArgumentedOpcall(),*/ d = {}, e = ArgumentedOpcall(0), f = {0};}
 }
 ```
 
 We have terribly many ways to initialize a struct (or union or class
 for that matter). I have trouble seeing the logic between all these.
I'm having trouble seeing why these are problematic. It basically just boils down to a couple of cases: 1) No initialization / initialize with a struct instance: just declare the variable, optionally assigning an instance of the struct (in this case, .init). This is normal and expected. Note that initializing with opCall falls under this category. 2) Initialize with brace syntax. This is one of the ways of initializing a struct. 3) Initialize with constructor syntax. This is the other way of initializing a struct. That's pretty much it. The only wrinkle in this picture is the interaction between .init and disable this(). Historically, before disable was introduced to language, ALL types have an .init value. It was something generic code could rely upon to get an instance of any type. Somewhere along the line, somebody twisted Walter's arm to add disable, to paper over the existence of .init by making it illegal to declare an instance of a type without explicitly constructing it. Unfortunately, a LOT of things in the language and its ecosystem had come to rely upon .init by then, and .init is so deeply entrenched in the compiler (and the language) that AFAIK it still exists in its innards somewhere even for ostensibly no-default-construction types. I.e., disable doesn't *completely* disable .init, it just hides it away (or tries to -- and not very completely, as you discovered). Compounding this imperfect implementation of disable is: https://issues.dlang.org/show_bug.cgi?id=7597 which is a long-standing issue (IMO bug) where users can willy-nilly declare their own definition of .init and thereby cause all sorts of pathological behaviour in the compiler & any code that relies upon .init to mean what it's supposed to mean. So there you have it, (1), (2), and (3) are the basic cases from which everything else derives. The weird cases are caused by compiler bugs and incomplete/imperfect implementation of disable and its unexpected/unwanted interactions with .init. T -- I am a consultant. My job is to make your job redundant. -- Mr Tom
Jul 28 2021
next sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
I also tried to get rid of the brace initialization of structs in favor of the 
() method, but some people really wanted the braces.
Jul 29 2021
parent Dukc <ajieskola gmail.com> writes:
On Thursday, 29 July 2021 at 09:02:16 UTC, Walter Bright wrote:
 I also tried to get rid of the brace initialization of structs 
 in favor of the () method, but some people really wanted the 
 braces.
Your named arguments DIP could at a quick glance finally make them obsolete... ...however, one problem: `struct`s with `static` `opCall`. If the `struct` in question has `const` or `immutable` members, the brace syntax is the only good way to initialize them. Without them you'd have to resort to ugly and unsafe casts.
Jul 29 2021
prev sibling parent Dukc <ajieskola gmail.com> writes:
On Wednesday, 28 July 2021 at 23:43:25 UTC, H. S. Teoh wrote:
 [snip]
Ok, that kind-of explains it, but not quite. I re-read the language spec and did some more tests. I try to wrap up the whole construction deal here. First off, every `struct` type has one, and only one initialization class. They are: literal initialized, constructed, call-overloaded, and call-disabled. The algrorithm to determine the type is: If a `struct` has any non-default constructors, it is constructed. Otherwise, If it has ` disable this()` it is call-disabled. A member with ` disable this()` does not count. Otherwise, if a struct has any `static opCall` overloads, it is call-overloaded. Otherwise it is literal-initialized. Any struct that has ` disable this()` is default-uninitializable. Also any struct that has a default-uninitializable member without default value is itself default-uninitializable. A default-uninitializable struct can only be default-initialized by explicitly assigning it's `.init` value to it. All other structs can be default-initialized as always. All default-initializable literal-initialized or constructed structs can be initialized with syntax `S()` which is the same as `S.init` (is/would be) if it's not redefined in S. Call-disabled `struct`s cannot obviously use the syntax in question. For call-overloaded `struct`s, `S()` results in the respective `opCall` if it exists. That function need not to return `S`. Otherwise, syntax `S()` is illegal for that struct. This applies regardless of whether the struct is default-initializable. C-style initialization syntax is legal for call-overloaded, call-disabled and literal-initialized `struct`s, but not for constructed structs. If any members are default-uninitializable without default value, they must be set. It does not matter if the struct itself is default-initializable. Syntax `S(a,b)` will call the respective `opCall` for call-overloaded `struct`s, the respective constructor for constructed ones, and act a struct literal for literal-initialized ones. The syntax in question is always forbidden for call-disabled `struct`s. Syntax `S s = a` where `!is(typeof(a) : S)` is forbidden for literal-initialized and call-disabled `struct`s (absent `opAssign` overloads at least - I didn't test them), but will be interpreted as `S s = S(a)` for constructed and call-overloaded `struct`s. Now, does this sound like a good summary? Any improvement ideas (either to summary or how things currently work)?
Jul 29 2021