Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implicit construction with associative array literals can result in more destructor calls than constructor calls #20542

Open
dlangBugzillaToGithub opened this issue Oct 30, 2024 · 0 comments

Comments

@dlangBugzillaToGithub
Copy link

Jonathan M Davis (@jmdavis) reported this on 2024-10-30T18:23:56Z

Transferred from https://issues.dlang.org/show_bug.cgi?id=24840

Description

I feel like there has to be a more reduced example of this, but I've failed thus far. In any case, this code (which has the extra strings so that it can print out what's happening even when the GC calls the destructor):

---
void main()
{
    import std.format;
    import std.stdio;
    import std.variant;

    static struct S
    {
        Variant v;
        string copyMsg;
        string destroyMsg;

        this(T)(T t)
            if(!is(immutable T == immutable S))
        {
            v = t;
            copyMsg = format("copy: %s", v);
            destroyMsg = format("destroy: %s", v);
        }

        this(this)
        {
            writeln(copyMsg);
        }

        ~this()
        {
            writeln(destroyMsg);
        }

        string toString()
        {
            return format("S(%s)", v);
        }
    }

    writeln(__LINE__);

    S[] expected = [
        ["x": S(1), "y": S(2)],
        ["x": S(3), "y": S(4)],
        ["x": S(5), "y": S(6)],
    ];

    writeln(__LINE__);
}
---

prints

---
46
destroy: ["y":S(6), "x":S(5)]
destroy: ["y":S(4), "x":S(3)]
destroy: ["y":S(2), "x":S(1)]
54
destroy: ["y":S(6), "x":S(5)]
destroy: ["y":S(4), "x":S(3)]
destroy: ["y":S(2), "x":S(1)]
destroy: 1
destroy: 2
destroy: 3
destroy: 4
destroy: 5
destroy: 6
---

So, 

---
    S[] expected = [
        ["x": S(1), "y": S(2)],
        ["x": S(3), "y": S(4)],
        ["x": S(5), "y": S(6)],
    ];
---

somehow ends up destroying the implicitly constructed S's in the array - and then they get destroyed again when the program exits. If there were a copy made in between, then it could be the copies getting destroyed, but there are no calls to the postblit constructor. So, either copies are being made without calling the postblit constructor, or the destructor is simply managing to be called multiple times somehow.

However, if we change that piece of the code to

---
    S[] expected = [
        S(["x": S(1), "y": S(2)]),
        S(["x": S(3), "y": S(4)]),
        S(["x": S(5), "y": S(6)]),
    ];
---

then we get

---
46
54
destroy: ["y":S(6), "x":S(5)]
destroy: ["y":S(4), "x":S(3)]
destroy: ["y":S(2), "x":S(1)]
destroy: 1
destroy: 2
destroy: 3
destroy: 4
destroy: 5
destroy: 6
---

This would be correct behavior. The number of copies and destructions match. It also indicates that no copies or destructions occurred when constructing the array, which is what I would have expected, but the important thing is that we have the same number of copies of the objects and destructions.

However, the big thing to note here is that this means that using the implicit construction syntax results in different behavior from using the explicit construction syntax when the two are supposed to be semantically identical. So, it would appear that something is going wrong when the compiler generates the code for the implicit construction.

I have yet to be able to reproduce this without a variant type (though I originally ran into it with a variant type at work rather than std.variant.Variant, so it's not specific to std.variant.Variant). So, I suspect that it has something to do with how S's are constructed inside the AA literals and then used to implicitly construct S's from those AA literals, but i don't know. I also haven't been able to reproduce it using just array literals.

That being said, I can say that if I change the constructor to

---
        this(int i)
        {
            v = i;
            copyMsg = format("copy: %s", v);
            destroyMsg = format("destroy: %s", v);
        }

        this(S[string] aa)
        {
            v = aa;
            copyMsg = format("copy: %s", v);
            destroyMsg = format("destroy: %s", v);
        }
---

it doesn't change the behavior, so the fact that S's constructor is templated doesn't seem to be affecting things (though maybe Variant's constructor is somehow).

All in all, this is just weird, but it does mean that we have a subtle footgun for folks who decide to use the implicit construction syntax with types that have a destructor.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant