Thread overview
Immovable types
Apr 19, 2017
Stanislav Blinov
Apr 19, 2017
sarn
Apr 19, 2017
kinke
Apr 19, 2017
Stanislav Blinov
Apr 19, 2017
Meta
Apr 19, 2017
Stanislav Blinov
April 19, 2017
Currently, we have the ability to disable postblit and/or assignments, thus create non-copyable types.
But it is always assumed that a value can be moved. Normally, this is great, as we don't have to deal with additional constructors explicitly. There are, however, occasions when move is undesirable (e.g. std.typecons.Scoped - class instance on the stack). What if a concept of immovable types was introduced? I.e. structs you can initialize, possibly copy, but never move. Having such types would e.g. disallow returning instances from functions, or make things like std.typecons.Scoped safe without relying on documented contract.
This would tie in with DIP1000, which seems not to propose using "scope" qualifier for type declarations.
Syntactically, this could be expressed by @disabling the rvalue ctor (e.g. @disable this(typeof(this))), similar to this() - a constructor which cannot be defined but can be @disable'd.

Consider:

// Code samples assume std.algorithm.move is additionally constrained
// w.r.t. disabled move construction

struct Scope(T)
{
    T value;
    this(T v) { value = v; }

    @disable this(Scope);
}

auto takesScope(Scope!int i) {}

auto usage()
{
    Scope!int i = 42;
    auto copyOfI = i;          // Ok, Scope is copyable
    takesScope(i);             // Ok, Scope is copyable
    takesScope(move(i));       // ERROR: Scope cannot be moved
    takesScope(Scope!int(10)); // Ok, constructed in-place
    return i;                  // ERROR: Scope cannot be moved
}

Non-copyable and immovable types will have to be explicitly initialized, as if they had @disable this(), as they can't even be initialized with .init:

struct ScopeUnique(T)
{
    T value;
    this(T v) { value = v; }

    @disable this(ScopeUnique);
    @disable this(this);
}

auto takesScopeUnique(ScopeUnique!int i) {}

auto usage()
{
    ScopeUnique!int i;                        // ERROR: i must be explicitly initialized
    ScopeUnique!int j = ScopeUnique!int.init; // ERROR: ScopeUnique is non-copyable
    ScopeUnique!int k = 42;                   // Ok
    k = ScopeUnique!int(30);                  // ERROR: ScopeUnique is non-copyable

    takesScopeUnique(k);                   // ERROR: ScopeUnique is non-copyable
    takesScopeUnique(move(k));             // ERROR: ScopeUnique cannot be moved
    takesScopeUnique(ScopeUnique!int(10)); // Ok, constructed in-place
    takesScopeUnique(ScopeUnique!int(ScopeUnique!int(10))); // ERROR: ScopeUnique cannot be moved
    return k;                              // ERROR: ScopeUnique cannot be moved.
}

This way, a type gains additional control over how it's instances can be passed around. At compile-time, it would help protect against escaping. At run-time, it opens a door for certain idioms, mainly more clearly expressing (transfer of) ownership.

It also brings certain symmetry: we already can differentiate between rvalue (copy) and lvalue assignments:

struct T
{
    this(int) {}
    void opAssign(T) {}
    void opAssign(ref T) {}
}

T t1, t2;
t1 = T(10);    // opAssign(T)
t2 = t1;       // opAssign(ref T)
t1 = move(t2); // opAssign(T)

but we cannot similarly differentiate the construction (move is always assumed to work):

T t;
T x = T(0);                    // this(int)
T y = t;                       // this(this)
T w = move(t);                 // ??? no constructor call at all

With the proposed capability, we would be able to impose or infer additional restrictions at compile time as to how an instance can be (is being) constructed.

I'd very much like to hear your thoughts on this, good/bad, if it already was proposed, anything. If it's found feasible, I could start a DIP. Destroy, please.
April 19, 2017
On Wednesday, 19 April 2017 at 02:53:18 UTC, Stanislav Blinov wrote:
> I'd very much like to hear your thoughts on this, good/bad, if it already was proposed, anything. If it's found feasible, I could start a DIP. Destroy, please.

I don't have comments about the syntax, but I did want this feature when writing Xanthe (https://gitlab.com/sarneaud/xanthe).  In normal D you can make a struct instance effectively immovable by dynamically allocating it, but I had to allocate stuff statically or on the stack.
April 19, 2017
On Wednesday, 19 April 2017 at 02:53:18 UTC, Stanislav Blinov wrote:
> But it is always assumed that a value can be moved.

It's not just assumed, it's a key requirement for structs in D, as the compiler can move stuff automatically this way (making a bitcopy and then eliding the postblit ctor for the new instance and the destructor for the moved-from instance).

That is quite a different concept to C++, where a (non-elided) special move ctor is required, moved-from instances need to be reset so that their (non-elided) destructor doesn't free moved-from resources etc.
April 19, 2017
On Wednesday, 19 April 2017 at 08:52:45 UTC, kinke wrote:
> On Wednesday, 19 April 2017 at 02:53:18 UTC, Stanislav Blinov wrote:
>> But it is always assumed that a value can be moved.
>
> It's not just assumed, it's a key requirement for structs in D, as the compiler can move stuff automatically this way (making a bitcopy and then eliding the postblit ctor for the new instance and the destructor for the moved-from instance).
>
> That is quite a different concept to C++, where a (non-elided) special move ctor is required, moved-from instances need to be reset so that their (non-elided) destructor doesn't free moved-from resources etc.

That's not quite correct. Copy elision in C++ also elides copy and move ctors and dtor.
Move ctors aren't a requirement, they *can* be defined to override default move semantics, or deleted to disable move construction.
That is concerning optimizations performed by the compiler. Library move(), both in C++ and in D, cannot elide the destructor, as the value already exists.
But move() in C++ and D is indeed different. In C++ it's just a cast, and it is up to the programmer to redefine the semantics if needed, or disable it. In D, we're not allowed to do either. I'm only proposing to relax the restriction in terms of disabling move, not introduce move ctors into D.
April 19, 2017
On Wednesday, 19 April 2017 at 02:53:18 UTC, Stanislav Blinov wrote:
> Non-copyable and immovable types will have to be explicitly initialized, as if they had @disable this(), as they can't even be initialized with .init:

It's an interesting idea but I can't even begin to fathom how much code this would break. So much D code relies on every type having a valid .init.
April 19, 2017
On Wednesday, 19 April 2017 at 14:45:59 UTC, Meta wrote:
> On Wednesday, 19 April 2017 at 02:53:18 UTC, Stanislav Blinov wrote:
>> Non-copyable and immovable types will have to be explicitly initialized, as if they had @disable this(), as they can't even be initialized with .init:
>
> It's an interesting idea but I can't even begin to fathom how much code this would break. So much D code relies on every type having a valid .init.

It should not break any existing code, unless it is using the syntax @disable this(typeof(this)), which, at the moment, is nonsensical, though not invalid.

Nor does it make .init invalid. Non-copyable immovables simply won't be able to explicitly initialize from it (it's an rvalue). We'll still be able to e.g. compare against .init, etc:

struct Immovable
{
    int value = 42;

    this(int v) { value = v; }

    @disable this(this);
    @disable this(Immovable);
}

assert(Immovable.init.value == 42);

Immovable i = 42;
assert(i == Immovable.init);