Jump to page: 1 2
Thread overview
Confusion regarding struct lifecycle
Feb 16, 2016
Matt Elkins
Feb 16, 2016
maik klein
Feb 16, 2016
Matt Elkins
Feb 16, 2016
Mike Parker
Feb 16, 2016
Marc Schütz
Feb 17, 2016
Matt Elkins
Feb 17, 2016
Matt Elkins
Feb 16, 2016
Ali Çehreli
Feb 17, 2016
Matt Elkins
Feb 17, 2016
Ali Çehreli
Feb 17, 2016
Matt Elkins
Feb 17, 2016
ZombineDev
Feb 17, 2016
Matt Elkins
Feb 17, 2016
anonymous
Feb 17, 2016
Ali Çehreli
Feb 18, 2016
ZombineDev
Feb 17, 2016
Ali Çehreli
February 16, 2016
I've been bitten again by my lack of understanding of the D struct lifecycle :-/. I managed to reduce my buggy program to the following example:

[code]
import std.stdio;

struct Foo
{
    @disable this();
    @disable this(this);

    this(int valueIn) {value = valueIn;}
    ~this() {writeln("Foo being destroyed: ", value);}

    int value;
}

struct FooList
{
    @disable this();
    @disable this(this);

    this(int)
    {
        writeln("Before 8");
        foos[0] = Foo(8);
        writeln("Before 1");
        foos[1] = Foo(1);
        writeln("Before 2");
        foos[2] = Foo(2);
        writeln("Before 3");
        foos[3] = Foo(3);
        writeln("After Foo construction");
    }

    Foo[5] foos;
}

unittest
{
    auto fooList = FooList(0);
    writeln("About to lose scope");
}
[/code]

[output]
Before 8
Before 1
Foo being destroyed: 0
Before 2
Foo being destroyed: 0
Before 3
Foo being destroyed: 0
After Foo construction
About to lose scope
Foo being destroyed: 0
Foo being destroyed: 3
Foo being destroyed: 2
Foo being destroyed: 1
Foo being destroyed: 8
[/output]

There are a few things which confuse me about this:
* Why does this code compile? In particular, I would have expected that with Foo[5] but initialization for only Foos 0 .. 3 and with the @disabled constructors in Foo, there would be a compiler error.
* Where do those first three destroyed Foos come from? I thought there should have been no Foos existing since default construction is @disabled...
* Even if somehow the Foos are being created despite the @disabled default constructor, why are only three Foos being destroyed before the scope is lost?

So I guess what I'm wondering is:
* If I @disable a default constructor on a struct, does the language guarantee that I won't have default-constructed instances of that struct? If not, what is the point of @disable for default constructors? If so, is the above situation a compiler bug or something I am missing?
* Is the below the right general syntax for creating an instance of a struct so as to avoid creating more than one copy? If not, what is?

Stack variable: auto foo = Foo(5);
Member variable: Foo m_foo; this(/* args */) {m_foo = Foo(5);}

Thanks!
February 16, 2016
On Tuesday, 16 February 2016 at 02:09:15 UTC, Matt Elkins wrote:
> I've been bitten again by my lack of understanding of the D struct lifecycle :-/. I managed to reduce my buggy program to the following example:
>
> [...]

In D you can always call Foo.init even with @disable this(), The first 3 destructor calls are from the 3 Foo.inits in your static array. I guess because you disabled the copy constructor, the Foo's will be moved and then they also need to call the destructor of the Foo.inits. Just like std::move does.
February 16, 2016
On Tuesday, 16 February 2016 at 03:31:51 UTC, maik klein wrote:
> In D you can always call Foo.init even with @disable this(),

Foo.init can be called implicitly (not just explicitly)? If so, why even have @disable this(), if it offers no guarantees?

> The first 3 destructor calls are from the 3 Foo.inits in your static array.

But why only 3? There are 5 Foos in the array, and 4 were explicitly overwritten...

February 16, 2016
On Tuesday, 16 February 2016 at 03:39:00 UTC, Matt Elkins wrote:
> On Tuesday, 16 February 2016 at 03:31:51 UTC, maik klein wrote:
>> In D you can always call Foo.init even with @disable this(),
>
> Foo.init can be called implicitly (not just explicitly)? If so, why even have @disable this(), if it offers no guarantees?

IMO, this is a bug. It should have to be explicit, just as it is with a single struct instance.
February 16, 2016
On 02/15/2016 06:09 PM, Matt Elkins wrote:

> * Where do those first three destroyed Foos come from?

I've printed some more information to come up with the following guess. I am not sure whether it is correct or intended. Just a guess...

When a temporary Foo object is moved into the array, the temporary object is set to Foo.init. This temporary object lives on the stack. In fact, all temporary Foo objects of Foo.this(int) live at the same location.

After Foo(8) is moved into the array and set to Foo.init, now Foo(1) is constructed on top of it. For that to happen, first the destructor is executed for the first life of the temporary, and so on...

There is one less Foo.init destruction because conceptually the initial temporary was not constructed on top of an existing Foo.init but raw memory.

The reason it makes sense to me is the fact that all Foo.init destructions happen on memory that is outside of the array:

import std.stdio;

struct Foo
{
    @disable this();
    @disable this(this);

    this(int valueIn) {
        value = valueIn;
        writefln("Foo being constructed: %s at %s", value, &this);
    }
    ~this() {writefln("Foo being destroyed  : %s at %s", value, &this);}

    int value;
}

struct FooList
{
    @disable this();
    @disable this(this);

    this(int)
    {
        writeln("foos.ptr: ", foos.ptr);
        writeln("Before 8");
        foos[0] = Foo(8);
        writeln("Before 1");
        foos[1] = Foo(1);
        writeln("Before 2");
        foos[2] = Foo(2);
        writeln("Before 3");
        foos[3] = Foo(3);
        writeln("Before 4");
        foos[4] = Foo(4);            // <-- Added by Ali
        writeln("After Foo construction");
    }

    Foo[10] foos;
}

unittest
{
    auto fooList = FooList(0);
    writeln("About to lose scope");
}

void main() {
}

Output:

foos.ptr: 7FFF15C01740
Before 8
Foo being constructed: 8 at 7FFF15C01740
Before 1
Foo being constructed: 1 at 7FFF15C016E0
Foo being destroyed  : 0 at 7FFF15C016A8  <-- temporary Foo.init
Before 2
Foo being constructed: 2 at 7FFF15C016E4
Foo being destroyed  : 0 at 7FFF15C016A8  <-- ditto
Before 3
Foo being constructed: 3 at 7FFF15C016E8
Foo being destroyed  : 0 at 7FFF15C016A8  <-- ditto
Before 4
Foo being constructed: 4 at 7FFF15C016EC  <-- Added by Ali
Foo being destroyed  : 0 at 7FFF15C016A8  <-- ditto
After Foo construction
About to lose scope
Foo being destroyed  : 0 at 7FFF15C01764
Foo being destroyed  : 0 at 7FFF15C01760
Foo being destroyed  : 0 at 7FFF15C0175C
Foo being destroyed  : 0 at 7FFF15C01758
Foo being destroyed  : 0 at 7FFF15C01754
Foo being destroyed  : 4 at 7FFF15C01750
Foo being destroyed  : 3 at 7FFF15C0174C
Foo being destroyed  : 2 at 7FFF15C01748
Foo being destroyed  : 1 at 7FFF15C01744
Foo being destroyed  : 8 at 7FFF15C01740

Ali

February 16, 2016
On Tuesday, 16 February 2016 at 04:00:27 UTC, Mike Parker wrote:
> On Tuesday, 16 February 2016 at 03:39:00 UTC, Matt Elkins wrote:
>> On Tuesday, 16 February 2016 at 03:31:51 UTC, maik klein wrote:
>>> In D you can always call Foo.init even with @disable this(),
>>
>> Foo.init can be called implicitly (not just explicitly)? If so, why even have @disable this(), if it offers no guarantees?
>
> IMO, this is a bug. It should have to be explicit, just as it is with a single struct instance.

There is likely some bug here. In the example, though, the elements _are_ constructed explicitly (except foos[4]). This is legitimate, as the first assignment of an element in a construct counts as construction.
February 17, 2016
On Tuesday, 16 February 2016 at 08:18:51 UTC, Ali Çehreli wrote:
> When a temporary Foo object is moved into the array, the temporary object is set to Foo.init. This temporary object lives on the stack. In fact, all temporary Foo objects of Foo.this(int) live at the same location.
>
> After Foo(8) is moved into the array and set to Foo.init, now Foo(1) is constructed on top of it. For that to happen, first the destructor is executed for the first life of the temporary, and so on...
>
> There is one less Foo.init destruction because conceptually the initial temporary was not constructed on top of an existing Foo.init but raw memory.

I guess that makes sense. But doesn't it imply that a copy is happening, despite the @disabled post-blit? The desired behavior was to construct the Foos in place, like with a C++ initializer list.

February 17, 2016
On Tuesday, 16 February 2016 at 10:45:09 UTC, Marc Schütz wrote:
> On Tuesday, 16 February 2016 at 04:00:27 UTC, Mike Parker wrote:
>> On Tuesday, 16 February 2016 at 03:39:00 UTC, Matt Elkins wrote:
>>> On Tuesday, 16 February 2016 at 03:31:51 UTC, maik klein wrote:
>>>> In D you can always call Foo.init even with @disable this(),
>>>
>>> Foo.init can be called implicitly (not just explicitly)? If so, why even have @disable this(), if it offers no guarantees?
>>
>> IMO, this is a bug. It should have to be explicit, just as it is with a single struct instance.
>
> There is likely some bug here. In the example, though, the elements _are_ constructed explicitly (except foos[4]). This is legitimate, as the first assignment of an element in a construct counts as construction.

The elements are not constructed explicitly with Foo.init; they are constructed explicitly with a user-defined Foo constructor. Since default construction leads to a semantically-invalid object in the non-reduced case, I was expecting that a @disabled default constructor would cause the compiler to complain on attempts to default-construct the struct. Preferably this would be on any attempt to do so, including explicit calls to Foo.init, but at a minimum I would want it to complain on attempts to do so implicitly. Otherwise there don't appear to be any useful guarantees offered by @disable this().
February 17, 2016
After some more time spent on (the non-reduced version of) this, I think there is a decent chance I am really just experiencing another manifestation of a bug I reported a little bit ago: https://issues.dlang.org/show_bug.cgi?id=15661

The good news is that this is now marked as resolved, so hopefully the next build will have the patch and maybe my problems will go away :).


February 16, 2016
On 02/16/2016 05:50 PM, Matt Elkins wrote:
> On Tuesday, 16 February 2016 at 08:18:51 UTC, Ali Çehreli wrote:
>> When a temporary Foo object is moved into the array, the temporary
>> object is set to Foo.init. This temporary object lives on the stack.
>> In fact, all temporary Foo objects of Foo.this(int) live at the same
>> location.
>>
>> After Foo(8) is moved into the array and set to Foo.init, now Foo(1)
>> is constructed on top of it. For that to happen, first the destructor
>> is executed for the first life of the temporary, and so on...
>>
>> There is one less Foo.init destruction because conceptually the
>> initial temporary was not constructed on top of an existing Foo.init
>> but raw memory.
>
> I guess that makes sense. But doesn't it imply that a copy is happening,
> despite the @disabled post-blit? The desired behavior was to construct
> the Foos in place, like with a C++ initializer list.
>

I changed my mind! :) This seems to be how D is being clever in constructors. I've just learned something (thank you!), which may very well be a bug rather than a feature.

The compiler analyzes the body of the constructor to differentiate between first use versus first initialization. For example, unlike C++, you can call super() anywhere in the constructor.

So, although this(this) is disabled, moving objects into a static-array member in a constructor must be allowed because otherwise we could not initialize such members. Since a static array must consist of .init values to begin with, every move into its members must also trigger its destructor if the type has elaborate destructor.

In this example, the compiler is being smart and skips one of the "move+destructor" operations. I felt this behavior in my other post where I had alluded to "raw memory".

This is what I've discovered: Try printing the static array at the beginning of the constructor:

    this(int)
    {
        writefln("Initial foos:");
        foreach (ref f; foos[]) {
            writeln(" ", f.value);
        }
        writeln("starting to assign");

        // ...
    }

Error:  field 'foos' initialization is not allowed in loops or after labels

So, this tells us (in a cryptic way) that the syntax foos[] at that point in the constructor is understood as "initialization" not as slicing. To contrast, now move that code below the Foo(8) assignment:


struct Foo
{
    // ...
    int value = 7;    // <-- Do this as well
}

    this(int)
    {
        writeln("Before 8");
        foos[0] = Foo(8);         // <-- FIRST ASSIGNMENT

        writefln("Initial foos:");
        foreach (ref f; foos[]) {
            writeln(" ", f.value);
        }
        writeln("starting to assign");

        // ...
    }

Now the code compiles and prints the contents of the array *without* any Foo destructor. (I think this is because Foo(8) is emplaced, rather than assigned.)

Before 8
Initial foos:
 8
 7
 7
 7
 7
starting to assign
Before 1
[...]

Only after that point we have an array of valid Foos where further assignments trigger destructors.

Ali

« First   ‹ Prev
1 2