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.

Reply via email to