www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Immovable types

reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
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.
Apr 18 2017
next sibling parent sarn <sarn theartofmachinery.com> writes:
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.
Apr 18 2017
prev sibling next sibling parent reply kinke <noone nowhere.com> writes:
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.
Apr 19 2017
parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
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.
Apr 19 2017
prev sibling parent reply Meta <jared771 gmail.com> writes:
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.
Apr 19 2017
parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
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);
Apr 19 2017