Thread overview
struct: default construction or lazy initialization.
Jan 31, 2017
bitwise
Jan 31, 2017
Ali Çehreli
Feb 01, 2017
bitwise
Feb 01, 2017
bitwise
Feb 01, 2017
Adam D. Ruppe
Feb 01, 2017
bitwise
Feb 01, 2017
kinke
Feb 01, 2017
bitwise
Feb 01, 2017
kinke
January 31, 2017
Unless I'm missing something, it seems that neither of these are actually possible.

Consider an object which needs internal state to function.
The obvious answer is to create it in the constructor:

struct Foo(T)
{
	T* payload;
	
	this() { payload = cast(T*)malloc(T.sizeof); }
	~this() { free(payload); }
	
	void foo() {
		// do something with payload that fails if not initialized
	}
}

But this is not possible in D, because structs can't have default constructors.

So one may think, I can use lazy initialization instead:

struct Foo(T)
{
	T* _payload;
	
	~this() { if(_payload) free(_payload); }
	
	@property T* payload() const {
		if(!_payload)
			(cast(Foo!T*)&this).payload = cast(T*)malloc(T.sizeof);
		
		return _payload;
	}
	
	void foo() {
		T* p = payload();
		// do something with payload that fails if not initialized
	}
	
	void bar() const {
		T* p = payload();
		// do something with payload that fails if not initialized
	}
}

So in C++, the above would be fine.
Since payload can never be perceived by the caller as uninitialized, the fact that it "breaks" const is irrelevant.

But you can't do this in D.

If the object is defined at module scope as shared static immutable, the compiler may put it in a readonly section of the executable which would cause an access violation upon trying to initialize it, and there is no way to prevent this from happening.

I'm hoping someone will tell me I'm wrong here, because the only alternative to the above approaches is to add boilerplate to _every_ _single_ _function_ that uses the payload in order to deal with separate cases where it's uninitialized.

Is there really no solution for this?
January 31, 2017
On 01/31/2017 03:15 PM, bitwise wrote:

> If the object is defined at module scope as shared static immutable

Yes, the situation is different from C++ but it's always possible to call a function (which constructor is one) to make the object.

It is indeed possible to initialize immutable objects by pure functions as done inside shared static this() below:

import core.stdc.stdlib;

struct Foo(T)
{
    T* payload;

    ~this() { free(payload); }

    void foo() {
        // do something with payload that fails if not initialized
    }
}

auto makeFoo(T)() {
    // This could be a static member function, even opCall().

    auto p = cast(T*)malloc(T.sizeof);
    *p = 42;
    return immutable(Foo!T)(cast(immutable(T)*)p);
}

shared static immutable(Foo!int) foo;

shared static this() {
    foo = makeFoo!int();
}

void main() {
    assert(*(foo.payload) == 42);
}

Ali

February 01, 2017
On Tuesday, 31 January 2017 at 23:52:31 UTC, Ali Çehreli wrote:
> On 01/31/2017 03:15 PM, bitwise wrote:
> [...]

Thanks for the response, but this doesn't really solve the problem.

> > If the object is defined at module scope as shared static immutable
> It is indeed possible to initialize immutable objects by pure functions as done inside shared static this() below:

I didn't mean that I wanted my object shared-static-immutable, but only that a solution would have to account for that possibility.

> Yes, the situation is different from C++ but it's always possible to call a function (which constructor is one) to make the object.

I'm saying that a caller should not have to explicitly initialize an object before use, but that a programmer should not have to add boilerplate to deal with zombie objects all over the place either.

A container for example:

struct Container(T) {
    void pushBack(T); // ok: mutable method, can lazily initialize payload.

    // not ok: container may be immutable
    Range!(const T) opSlice() const;
    Iterator!(const T) find(T) const;
    bool empty() const;
    size_t count() const;
}

Container!int c; // = Container!int() -> can't do this.
if(c.empty) // can't initialize here either..
     c.pushBack(1);

Such innocent looking code will fail without boilerplate inserted everywhere.
-I can't lazily initialize the container in "empty()".
-I can't pre-emptively initialize it in a default constructor.

This problem causes the propagation of null checks all over the place.
Objects returned from the container will have to have a "zombie" state as well, and check validity at each use.

I wouldn't classify this as "a difference", but as a hole.

Although I don't remember where, I recently saw a discussion about how "mutable" may possibly be implemented. IIRC, there was no solution stated in that discussion.

The only solution that comes to mind would be to somehow relax the constraints of const, and make it possible to prevent a struct from being declared immutable, so that lazy initialization could be done.

Recent discussions seem to indicate structs having default ctors is not an option.
February 01, 2017
C#'s "Dispose" pattern comes to mind here.

You don't leak memory, you just leak file handles and graphics resources instead when you forget to explicitly call Dispose().

February 01, 2017
On Wednesday, 1 February 2017 at 00:43:39 UTC, bitwise wrote:
> Container!int c; // = Container!int() -> can't do this.

Can you live with

Container!int c = Container!int.create();

because D supports that and can force the issue with `@disable this();` which causes compilation to fail any place where it isn't explicitly initialized.
February 01, 2017
On Wednesday, 1 February 2017 at 01:52:40 UTC, Adam D. Ruppe wrote:
> On Wednesday, 1 February 2017 at 00:43:39 UTC, bitwise wrote:
>> Container!int c; // = Container!int() -> can't do this.
>
> Can you live with
>
> Container!int c = Container!int.create();
>
> because D supports that and can force the issue with `@disable this();` which causes compilation to fail any place where it isn't explicitly initialized.

I suppose this works, but to be honest, I wouldn't use it.

I really don't feel like I'm asking to "have my cake and eat it too" by expecting a proper solution for this.

The current behavior doesn't even really make sense.

Example:

struct S  {
    // this(){}
    this(Args...)(auto ref Args args) { writeln("ctor"); }
    ~this() { writeln("dtor"); }
}

void foo(Args...)(auto ref Args args) { writeln("foo"); }

int main(string[] argv) {
    S s;
    S s2 = S();
    foo();
    return 0;
}

outputs:
  foo
  dtor
  dtor

I would expect that I could at least have this() invoked  for 's2', but I can't even declare it at all. So while 'S()' looks like a constructor call, it doesn't call one. Instead, the current behavior forces explicit initialization of objects, pointless boilerplate, or unorthodox/unreliable workarounds.

Even more confusingly, the above example prints "foo" but not "ctor", because calling variadic functions with no arguments is fine - except for constructors.

Finally, destructors are currently called on objects which were never constructed. You can't even call what's going on with structs RAII at this point.




February 01, 2017
On Wednesday, 1 February 2017 at 23:02:11 UTC, bitwise wrote:
> On Wednesday, 1 February 2017 at 01:52:40 UTC, Adam D. Ruppe wrote:
>> On Wednesday, 1 February 2017 at 00:43:39 UTC, bitwise wrote:
>>> Container!int c; // = Container!int() -> can't do this.
>>
>> Can you live with
>>
>> Container!int c = Container!int.create();
>>
>> because D supports that and can force the issue with `@disable this();` which causes compilation to fail any place where it isn't explicitly initialized.
>
> I suppose this works, but to be honest, I wouldn't use it.

I rather wouldn't have to live with this limitation as well. The problem is that if my struct T can only be correctly initialized via static factory, all aggregates containing a field of type T (directly or indirectly!) will then have to have a static factory as well => no real RAII.

> The current behavior doesn't even really make sense.
>
> Example:
>
> struct S  {
>     // this(){}
>     this(Args...)(auto ref Args args) { writeln("ctor"); }
>     ~this() { writeln("dtor"); }
> }
>
> void foo(Args...)(auto ref Args args) { writeln("foo"); }
>
> int main(string[] argv) {
>     S s;
>     S s2 = S();
>     foo();
>     return 0;
> }
>
> outputs:
>   foo
>   dtor
>   dtor
>
> I would expect that I could at least have this() invoked  for 's2', but I can't even declare it at all. So while 'S()' looks like a constructor call, it doesn't call one. Instead, the current behavior forces explicit initialization of objects, pointless boilerplate, or unorthodox/unreliable workarounds.
>
> Even more confusingly, the above example prints "foo" but not "ctor", because calling variadic functions with no arguments is fine - except for constructors.
>
> Finally, destructors are currently called on objects which were never constructed. You can't even call what's going on with structs RAII at this point.

It's not that bad. D just doesn't support a default ctor for structs at all and simply initializes each instance with T.init. Your `s2` initialization is most likely seen as explicit default initialization (again with T.init). Destructing both instances is exactly what should happen.

Can anyone point to the rationale for not supporting default constructors for structs? It prevents true RAII and hinders C++ interop.
February 01, 2017
On Wednesday, 1 February 2017 at 23:24:27 UTC, kinke wrote:
> It's not that bad. D just doesn't support a default ctor for structs at all and simply initializes each instance with T.init. Your `s2` initialization is most likely seen as explicit default initialization (again with T.init). Destructing both instances is exactly what should happen.


I was going to add a point about this.

1| S s1;
2| S s2 = S();

The effect of line 1 and 2 are exactly the same - which is that the lhs ends up with S.init. S.this() should either be called at line 2, or the syntax of line 2 should be forbidden on the grounds that default struct ctors cannot be declared.
February 01, 2017
On Wednesday, 1 February 2017 at 23:32:12 UTC, bitwise wrote:
> On Wednesday, 1 February 2017 at 23:24:27 UTC, kinke wrote:
>> It's not that bad. D just doesn't support a default ctor for structs at all and simply initializes each instance with T.init. Your `s2` initialization is most likely seen as explicit default initialization (again with T.init). Destructing both instances is exactly what should happen.
>
>
> I was going to add a point about this.
>
> 1| S s1;
> 2| S s2 = S();
>
> The effect of line 1 and 2 are exactly the same - which is that the lhs ends up with S.init. S.this() should either be called at line 2, or the syntax of line 2 should be forbidden on the grounds that default struct ctors cannot be declared.

Yep, they are the same. Initialization with S.init is a trivial blit from a compile-time constant, so there's no call to a default ctor. This in turn makes construction of static arrays and structs containing a field of type S trivial, as no default ctor has to be generated if a field has one, there are no potential exceptions etc.

Ideally, S.this() would be called in both cases (just like C++). The explicit syntax in line 2 is useful for stuff like `const s = S();`.