Please fix `.init` property

Jonathan M Davis newsgroup.d at jmdavisprog.com
Mon Jan 8 05:22:20 UTC 2024


On Sunday, January 7, 2024 9:14:48 PM MST matheus via Digitalmars-d wrote:
> On Sunday, 7 January 2024 at 23:12:30 UTC, Hipreme wrote:
> > ...
>
> For what it seems this happens only with arrays?
>
> This happened with for example int[] a; but when I tried with
> other types (Non-arrays) like (string, int) this problem didn't
> occur.
>
> Since like in this example string arrays are pointers, I think
> something was messed-up with the address.
>
> Matheus.

Well, to an extent, the problem here is simply that the member variable is a
type with mutable indirections. The dynamic array itself is just a pointer
and a length, and mutating what it points to like the OP's example doesn't
actually mutate the init value. In order to avoid that sort of mutation,
initializing the struct would need to give you a deep copy rather than a
shallow copy. Other member variables with mutable indirections would have
similar problems. For instance, all of the assertions in this code pass:

class C
{
    string foo = "foo";
}

struct S
{
    C c = new C;
}

void main()
{
    S s1;
    assert(s1.c.foo == "foo");
    assert(s1.c.foo == S.init.c.foo);

    S s2;
    s2.c.foo = "bar";
    assert(s2.c.foo == "bar");

    assert(s1.c.foo == "bar");
    assert(S.init.c.foo == "bar");

    assert((cast(void*)s1.c) is cast(void*)s2.c);
    assert((cast(void*)s1.c) is cast(void*)S.init.c);

    S s3;
    s3.c = new C;
    assert(s3.c.foo == "foo");

    assert(s1.c.foo == "bar");
    assert(s2.c.foo == "bar");
    assert(S.init.c.foo == "bar");

    assert((cast(void*)s1.c) is cast(void*)s2.c);
    assert((cast(void*)s1.c) is cast(void*)S.init.c);

    assert((cast(void*)s3.c) !is cast(void*)S.init.c);
}

The init value itself is never mutated, but what it points to is. Someone
could interpret it like the init value had mutated, because what it's
pointed to changed, and therefore, accessing the values via init then
results in different values that were there at the start of the program, but
technically, init itself never changed.

As such, the OP should expect that mutating values in the array would affect
any other variable when it's default-initialized, and I don't think that
it's realistic for it to work any other way, because that requires making a
deep copy of the init value rather than a shallow copy, and that's not
something that can be done in the general case, because D doesn't have a
mechanism for that.

However, in spite of all of that, something weird is going on with the OP's
example, because while variables that are created after the element of the
array has been mutated see that mutation, the init value itself does not.
So, it would appear that the array in the init value itself somehow ends up
pointing to a different block of memory than the array does when the struct
is default initilaized. This example shows the same problem

struct S
{
    int[] arr = [1, 2, 3];
}

void main()
{
    S s1;
    assert(s1.arr == [1, 2, 3]);
    assert(s1.arr == S.init.arr);

    S s2;
    s2.arr[0] = 42;
    assert(s2.arr == [42, 2, 3]);

    assert(s1.arr == [42, 2, 3]);
    assert(S.init.arr == [1, 2, 3]);
}

and if I were to change main to

void main()
{
    import std.stdio;
    writeln(S.init.arr.ptr);

    S s1;
    writeln(s1.arr.ptr);

    S s2;
    writeln(s2.arr.ptr);

    writeln(S.init.arr.ptr);
}

running it on my computer results in

C378D421000
2C3F08
2C3F08
C378D421010

So, for some reason, the ptr of the array in the init value has a different
address from the one in the struct's after they've been default-initialized,
but the struct's get the same pointer value.

This is in stark contrast to my previous example where the member variable
that was a class reference ended up with the address being the same for both
the init value and the default-initialized structs. So, it would appear that
the compiler or runtime is doing something different with dynamic arrays
than it is with classes, and the behavior definitely seems wrong, because it
means that a default-initialized struct does not match its init value - and
this without worrying about mutating anything. e.g. this assertion fails
when it should never fail

struct S
{
    int[] arr = [1, 2, 3];
}

void main()
{
    S s;
    assert(s is S.init);
}

So, I would say that there is definitely a bug here with regards to init
values when a struct has a member variable which is a dynamic array.

However, ultimately, that's not really what the OP seems to be complaining
about. Rather, he seems to be complaining about the fact that if you mutate
a member variable with mutable indirections which were not null in the init
value, then the changes to those mutable indirections are visible via the
init value, and I don't think that that's something that's ever going to
change, because it would require that default initialization do a deep copy
rather than a shallow copy. Obviously, it can be surprising if it's not
something that you've run into or thought through before, but D doesn't have
a generic way to do deep copies, and you wouldn't necessarily want a deep
copy in all cases anyway, so even if we could make it do a deep copy, that
wouldn't necessarily be a good change for the language as a whole, though it
would certainly fix the issue that the OP ran into.

Arguably, what the OP needs is a default constructor rather than an init
value, but that's not the way that D is designed, and it would be
problematic with some of its features to use default construction instead
(though this is far from the only case where not having default construction
for structs can be annoying).

- Jonathan M Davis





More information about the Digitalmars-d mailing list