www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - struct: default construction or lazy initialization.

reply bitwise <bitwise.pvt gmail.com> writes:
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?
Jan 31
parent reply =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
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
Jan 31
parent reply bitwise <bitwise.pvt gmail.com> writes:
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.
Jan 31
next sibling parent bitwise <bitwise.pvt gmail.com> writes:
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().
Jan 31
prev sibling parent reply Adam D. Ruppe <destructionator gmail.com> writes:
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.
Jan 31
parent reply bitwise <bitwise.pvt gmail.com> writes:
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.
Feb 01
parent reply kinke <noone nowhere.com> writes:
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.
Feb 01
parent reply bitwise <bitwise.pvt gmail.com> writes:
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.
Feb 01
parent kinke <noone nowhere.com> writes:
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();`.
Feb 01