D issues are now tracked on GitHub. This Bugzilla instance remains as a read-only archive.
Issue 19928 - disallow modification of immutable in constructor after calling base ctor
Summary: disallow modification of immutable in constructor after calling base ctor
Status: NEW
Alias: None
Product: D
Classification: Unclassified
Component: dmd (show other issues)
Version: D2
Hardware: x86 All
: P4 enhancement
Assignee: No Owner
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2019-05-31 14:38 UTC by Steven Schveighoffer
Modified: 2024-12-13 19:03 UTC (History)
2 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this issue.
Description Steven Schveighoffer 2019-05-31 14:38:23 UTC
This breaks immutability:

import std.stdio;
class C
{
    void foo() { writeln("C");}
    this()
    {
        foo();
    }
}

class D : C
{
    immutable int x;
    this()
    {
        super();
        x = 5;
        foo();
    }
    override void foo()
    {
        writeln("D: ", x);
    }
}

void main()
{
    new D;
}

x could be considered final once any base ctor is called, or really any function which accepts a path back to x (like if you call some external bar(this), it should be the same thing).
Comment 1 RazvanN 2019-06-04 10:12:24 UTC
I don't think this issue is valid. If we disallow modification of immutable fields after a base class ctor is called then it will be impossible to initialize that field after a super call, which in my opinion is unacceptable behavior. What happens here is that foo is called before the field is actually initialized so it reads the default value of x, then it is called again after x has been initialized. This behavior is correct. The code in the original post is similar to this:

=============================
import std.stdio : writeln;

struct A
{
    immutable int x;
    this(int)
    {
        foo();
        x = 8;
        foo();
    }

    void foo()
    {   
        writeln(x);
    }       
}

void main()
{
    A a = A(2);
}
=============================

This code compiles and runs successfully. I don't see why it wouldn't. An alternative approach would be to consider that the first use of x locks down the variable and future accesses to it are considered modifications, but this leads to other problems: the constructor will not be able to initialize x and it would require inter-function compilation to determine this; dmd does not support inter-function compilation.

I suggest we close this as invalid.
Comment 2 Andrei Alexandrescu 2019-06-04 15:52:23 UTC
This does seem to be a problem. This is liable to cause problems:

import std.stdio : writeln;

struct A
{
    immutable int x;
    this(int)
    {
        foo();
        x = 8;
        foo();
    }

    void foo()
    {   
        passToAnotherThread(&x);
    }       
}

The observing thread assumes the immutable(int)* it receives is, well, immutable. In reality that value will change over time.
Comment 3 Andrei Alexandrescu 2019-06-04 15:58:11 UTC
A solution seems to be disallowing the use of "this" and "this.xyz" (as in passing as an argument to methods or function) until the object is cooked. Cooked means all immutable (or otherwise restricted) fields are initialized.

Implications for other qualifiers and combinations need to be analyzed.
Comment 4 Andrei Alexandrescu 2019-06-04 16:01:49 UTC
Hm, this does not solve the original problem though.
Comment 5 Andrei Alexandrescu 2019-06-04 16:05:27 UTC
So a solution to the super() issue would be to require the immutable fields are assigned before calling super(). It is odd but it works.
Comment 6 Steven Schveighoffer 2019-06-04 20:49:02 UTC
The reason you can modify immutable in a constructor is because nothing else has access to it. As soon as you give access to it elsewhere, it can cause a problem. Andrei is correct, calling any function which allows access to the immutable provides a vector of breaking immutability.

(In reply to RazvanN from comment #1)
> I don't think this issue is valid. If we disallow modification of immutable
> fields after a base class ctor is called then it will be impossible to
> initialize that field after a super call, which in my opinion is
> unacceptable behavior.

It's tricky, and definitely difficult to deal with. It would disallow certain solutions that seem otherwise valid. But that might be the cost of having a correct immutable implementation.

I will note that Swift enforces all members are initialized (look for 2-phase initialization) before calling a super ctor:
https://docs.swift.org/swift-book/LanguageGuide/Initialization.html

Java is the opposite, it requires you call any super ctor FIRST before initializing members: https://stackoverflow.com/questions/15682457/initialize-field-before-super-constructor-runs

But they also don't have a concept of immutable which is implicitly sharable.
Comment 7 Andrei Alexandrescu 2019-06-04 20:58:49 UTC
I'm glad Swift provides a precedent and inspiration. We should follow their model.
Comment 8 dlangBugzillaToGithub 2024-12-13 19:03:38 UTC
THIS ISSUE HAS BEEN MOVED TO GITHUB

https://github.com/dlang/dmd/issues/19573

DO NOT COMMENT HERE ANYMORE, NOBODY WILL SEE IT, THIS ISSUE HAS BEEN MOVED TO GITHUB