Thread overview
Struct initialization is a mess
Jul 28, 2021
Dukc
Jul 28, 2021
H. S. Teoh
Jul 29, 2021
Walter Bright
Jul 29, 2021
Dukc
Jul 29, 2021
Dukc
July 28, 2021

In the main function, I have commented out the initialization forms that don't compile. Everything else compiles.

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 opCalled structs 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.

July 28, 2021
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
July 29, 2021
I also tried to get rid of the brace initialization of structs in favor of the () method, but some people really wanted the braces.
July 29, 2021

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 structs cannot obviously use the syntax in question.

For call-overloaded structs, 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 structs, 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 structs, 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 structs.

Syntax S s = a where !is(typeof(a) : S) is forbidden for literal-initialized and call-disabled structs (absent opAssign overloads at least - I didn't test them), but will be interpreted as S s = S(a) for constructed and call-overloaded structs.

Now, does this sound like a good summary? Any improvement ideas (either to summary or how things currently work)?

July 29, 2021

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: structs 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.