Strange compiler error. Whose bug is that?

Jonathan M Davis newsgroup.d at jmdavisprog.com
Sat Jan 27 16:04:57 UTC 2018


On Saturday, January 27, 2018 09:35:05 Oleksii Skidan via Digitalmars-d-
learn wrote:
> On Saturday, 27 January 2018 at 08:18:07 UTC, thedeemon wrote:
> > On Friday, 26 January 2018 at 21:17:14 UTC, Oleksii Skidan
> >
> > wrote:
> >> struct Game {
> >>
> >>     Triangle player = new Triangle;
> >
> > When you initialize a struct member like this, compiler tries
> > to calculate the initial value and remember it as data, so each
> > time such struct is constructed the data is just copied. Which
> > means this data must be computable at compile time, however
> > your Triangle constructor is using pointers to some values,
> > these pointers will only be known at run time. This means you
> > need to construct Triangles at run time, in Game constructor,
> > not at compile time in this initialization syntax.
>
> Got it. But are reference-types "computable" at compile time at
> all? Shouldn't they be relying on D runtime?
>
> To my understanding Triangle instantiation happens when Game
> constructor is called. I assume that D runtime has been
> initialized already, and thus there should be a valid GC and it
> should be fine to instantiate a reference-type.
>
> As well, if I'm wrong about Game constructor, then compiler
> generated errors are wrong and misleading. The compiler should be
> swearing at `Triangle player = new Triangle;`, or not?

I don't think that "computable" is really the best way to look at this. It's
more an isssue of the value being carried over from compile time to runtime.
But I'll try to explain it. You're clearly assuming that some stuff is
happening at runtime that happens at compile time.

Module-level variables, static variables, and member varibales which are
directly initialized must all have their values known at compile time. If
you have something like

static int i = foo();

or

struct S
{
    int i = foo();
}

then the compiler must know the value of i at compile time. As such, foo
must be run at compile time so that the resulting value can be known and
stored in the program. In the case of a member variable of a struct, that's
in the init value of the struct (it's the same for classes except that you
don't have access to it, since with classes, you always operate on
references, never the class itself, and the class reference's init value is
null; the underlying object still has an init value though).

For module-level variables and static variables, the runtime may need to do
some stuff on program or thread start-up to fill in the values, but they're
all known at compile time. For member variables, the object is initialized
to the type's init value when the object is created, and then if a
constructor is used, it is run. In the case of

S s;

no constructor is run (s is just filled in with the init value), whereas
with something like

auto s = S("hello");

or

auto c = new MyClass(42);

the object is filled in with the init value and then a constructor is run.
Either way, if you have

struct S
{
    int i = foo();
}

foo was run at compile time and not rerun at runtime. Its result is part of
S.init.

If a value is set in the constructor, e.g.

struct S
{
    int i = foo();
    string s;

    this(string s)
    {
        this.s = s;
    }
}

then that's done at runtime, but all of the direct initializions are done at
compile time when determining the value of S.init.

Given how all of this works, it's actually king of crazy that reference
types work at all. Consider,

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

arr must be known at compile time, and yet are is just a pointer and a
length pointing to heap memory. Heap memory isn't part of the executable.
It's part of a specific run of the program. So, somehow, the value of the
memory that arr refers to needs to be calculated at compile time and then
reconstructed at runtime. The runtime has to recreate it in memory from the
value that was known at compile time. It's not going to rerun any functions,
so if you have

struct S
{
    int[] arr = foo();
}

foo still has to have been run at compile time. The resulting value will
need to have been stored somehow so that S.init can contain a normal dynamic
array that points to heap memory at runtime.

For the compiler to work like this with dynamic arrays, it had to have had
work done specifically for dynamic arrays so that the runtime would know how
to reconstruct them. That's not something that can easily be done for
arbitrary types - and it hasn't even been done for some of the simple stuff,
though what has been done has increased over time. e.g.

struct S
{
    int* i = new int(42);
}

will not compile. But for arbitrarily complex user-defined types, it gets
far more complex. So, if you have something like

struct S
{
    auto myClass = new MyClass(42);
}

the compiler does not transfer the value of the class reference at compile
time to runtime. In years past, I would have simply explained the compiler
and runtime don't understand how to reconstruct a class like that (heck, the
language doesn't even have a built-in way to copy a class - just its
reference). However, a few years back, the compiler and runtime were
actually improved improved enough that they're able to take a const or
immutable class and reconstruct it at runtime. So,

struct S
{
    immutable myClass = new immutable MyClass(4);
}

would work. But it still doesn't work with mutable classes, and arguably,
it really shouldn't do it with mutable, dynamic arrays either. Consider

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

void main()
{
    S s1;
    S s2;
    assert(s1.arr.ptr == s2.arr.ptr);
    assert(s1.arr == [1, 2, 3, 4]);
    s2.arr[3] = 5;
    assert(s1.arr == [1, 2, 3, 5]);
}

That code passes. The value of s1.arr comes from S.init.arr, so every single
instance of S has an identical value for arr. With the nature of dynamic
arrays, if you start appending to one, it doesn't append to the others, or
if you assign the array itself a new value, it won't affect the arrays in
the other stucts, but if you don't assign a new value to the array, and you
mutate any of the elements, they will be mutated for all (at least so long
as the array wasn't reallocated due to appending). That's almost certainly
not the behavior that the programmer wants. It comes naturally from what
S.init is and how an object is initialized, but it's not exactly desirable
behavior. If the compiler required that direct initializations of member
variables which were arrays be const, tail-const, immutable, or
tail-immutable like it does with class references, then there wouldn't be a
problem, but unfortunately, it doesn't.

So, if you want a struct or class to have a member variable which is a
mutable reference type, then you shouldn't be directly initializing it (and
in most cases, can't). You should be initializing its value in the struct or
class' constructor. That can get a bit annoying with a struct in that
struct's don't have default constructors in D, but you'll need to either
have a non-default constructor run or a factory function (or just explicitly
assign it after the object is initialized) if you want the member variable
to be a usable value.

- Jonathan M Davis



More information about the Digitalmars-d-learn mailing list