November 27, 2018
On 11/27/18 3:00 AM, Sebastiaan Koppe wrote:
> I have a non-copyable struct and I really want a compiler error whenever I access it after it has been moved.
> 
> ---
> struct Handle {
>      ...
>      @disable this(this);
>      ...
> }
> 
> void main() {
>      import std.algorithm : move;
>      auto handle = getOne();
>      auto second = handle.move;  /// line 14
>      auto third = handle.move;    ///  <- compiler error, variable handle is invalid after line 14
> }
> ---
> 
> I believe this would prevent some nasty bugs when dealing with these structs.
> 
> What do you think?

You can do a runtime error (with opPostMove included), but I think a compiler error would require full flow analysis, and would have to default to allowing it, as otherwise you will get false errors that are more annoying than the original problem.

-Steve
November 27, 2018
On Tuesday, 27 November 2018 at 15:27:19 UTC, Steven Schveighoffer wrote:
> You can do a runtime error (with opPostMove included), but I think a compiler error would require full flow analysis, and would have to default to allowing it, as otherwise you will get false errors that are more annoying than the original problem.

A good modern compiler of a statically compiled language has to have flow analysis enabled unconditionally. I don't see any reason not to enable it in D frontend, except Walter's stubbornness. The current situation with D doesn't even make sense: with optimizations enabled, the compiler can reject code that compiles just fine without them. I find it sad and ridiculous at the same time. This happens exactly because dataflow analysis is only enabled when optimizations are enabled:

void main()
{
    int* x;
    *x = 0;
}

This compiles just fine with `dmd x.d`, but emits an error with `dmd -O x.d`:
Error: null dereference in function _Dmain
November 27, 2018
On 11/27/18 11:49 AM, burjui wrote:
> On Tuesday, 27 November 2018 at 15:27:19 UTC, Steven Schveighoffer wrote:
>> You can do a runtime error (with opPostMove included), but I think a compiler error would require full flow analysis, and would have to default to allowing it, as otherwise you will get false errors that are more annoying than the original problem.
> 
> A good modern compiler of a statically compiled language has to have flow analysis enabled unconditionally.

Does g++ count as a modern compiler? It doesn't do flow analysis on the code you have below.

> I find it sad and ridiculous at the same time. This happens exactly because dataflow analysis is only enabled when optimizations are enabled:
> 
> void main()
> {
>      int* x;
>      *x = 0;
> }
> 
> This compiles just fine with `dmd x.d`, but emits an error with `dmd -O x.d`:
> Error: null dereference in function _Dmain

Note that complete flow analysis is equivalent to the halting problem. So really, you can never get it perfect, and therefore, there are always going to be some things you can't catch. So it's impossible to fulfill that promise in any compiler.

In any case, I can't imagine how to do it without some handshaking between std.algorithm.move and the compiler, which is likely to be met with resistance.

-Steve
November 27, 2018
On Tue, Nov 27, 2018 at 1:08 AM Stanislav Blinov via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
>
> On Tuesday, 27 November 2018 at 08:37:32 UTC, Nicholas Wilson wrote:
> > On Tuesday, 27 November 2018 at 08:00:22 UTC, Sebastiaan Koppe wrote:
> >> I have a non-copyable struct and I really want a compiler error whenever I access it after it has been moved.
> >>
> >> ---
> >> struct Handle {
> >>     ...
> >>     @disable this(this);
> >>     ...
> >> }
> >>
> >> void main() {
> >>     import std.algorithm : move;
> >>     auto handle = getOne();
> >>     auto second = handle.move;  /// line 14
> >>     auto third = handle.move;    ///  <- compiler error,
> >> variable handle is invalid after line 14
> >> }
> >> ---
> >>
> >> I believe this would prevent some nasty bugs when dealing with these structs.
> >>
> >> What do you think?
> >
> > we need an `@invalidate`s attribute for that, would apply to pointers passed to free etc. Probably would need a DIP though. I like it.
>
> Yeah, that could be an awesome addition. However, it's not as simple, because this should be legal too:
>
> ```
> auto handle = getOne();
> auto second = handle.move; // `handle` becomes invalid
> // ...
> handle = getOne(); // `handle` is valid again
> ```

The language goes to great effort to return everything to it's `init`
state after being moved or destroyed. The whole point of that is to
cover the cases you are concerned about here.
If it was invalid to access a thing after it's moved (or after
destruction), then there's no reason for any of the work that resets
thing to their `init` to happen... that's quite a different set of
language semantics.
November 27, 2018
On Tuesday, 27 November 2018 at 19:51:12 UTC, Manu wrote:
> The language goes to great effort to return everything to it's `init`
> state after being moved or destroyed. The whole point of that is to
> cover the cases you are concerned about here.
> If it was invalid to access a thing after it's moved (or after
> destruction), then there's no reason for any of the work that resets
> thing to their `init` to happen... that's quite a different set of
> language semantics.

Yeah, I also totally fail to see a reason why a moved-from instance should be treated any different than a default-allocated one. Ownership taken away, reset to init, memory obviously still claimed and accessible as long as in scope.
November 27, 2018
On Tuesday, November 27, 2018 12:51:12 PM MST Manu via Digitalmars-d wrote:
> The language goes to great effort to return everything to it's `init`
> state after being moved or destroyed. The whole point of that is to
> cover the cases you are concerned about here.
> If it was invalid to access a thing after it's moved (or after
> destruction), then there's no reason for any of the work that resets
> thing to their `init` to happen... that's quite a different set of
> language semantics.

I'd forgotten that move reset the variable to its init value. But given that it does, that pretty much does eliminate this entire problem. I can understand it if someone would still consider it a bug in their code to reuse the variable given that it then doesn't hold the value that it did before, but it's perfectly @safe to access it at that point, and memory safety issues would have been the biggest reason to be concerned about touching the variable again after it's been moved. As such, having the compiler flag a variable being used after a move would be pretty much equivalent to having it flag it if you used a variable that was simply default-initialized rather than having been given an explicit value.

- Jonathan M Davis



November 28, 2018
On Tuesday, 27 November 2018 at 19:28:13 UTC, Steven Schveighoffer wrote:
>> void main()
>> {
>>      int* x;
>>      *x = 0;
>> }
>> 
>> This compiles just fine with `dmd x.d`, but emits an error with `dmd -O x.d`:
>> Error: null dereference in function _Dmain
>
> Note that complete flow analysis is equivalent to the halting problem. So really, you can never get it perfect, and therefore, there are always going to be some things you can't catch. So it's impossible to fulfill that promise in any compiler.

It's possible if you're willing to make the rules strict enough that some otherwise-valid programs get rejected--that's what Rust does, after all. But for D, that isn't really an option.
November 28, 2018
On Tuesday, 27 November 2018 at 22:49:44 UTC, kinke wrote:
> Yeah, I also totally fail to see a reason why a moved-from instance should be treated any different than a default-allocated one.

You are right, it shouldn't. A Handle.init or one after a move are both invalid. Invalid from the standpoint that a Handle should always refer to some underlying resource. And when it provably doesn't, I want to compiler to help me.
November 28, 2018
On Wednesday, 28 November 2018 at 00:28:35 UTC, Jonathan M Davis wrote:
> As such, having the compiler flag a variable being used after a move would be pretty much equivalent to having it flag it if you used a variable that was simply default-initialized rather than having been given an explicit value.

Yep. Exactly.

I would love to opt-in such a feature via e.g.:

struct Handle {
   ...
  @disable Handle.init;
  ...
}
November 28, 2018
On Wednesday, November 28, 2018 12:48:54 AM MST Sebastiaan Koppe via Digitalmars-d wrote:
> On Wednesday, 28 November 2018 at 00:28:35 UTC, Jonathan M Davis
>
> wrote:
> > As such, having the compiler flag a variable being used after a move would be pretty much equivalent to having it flag it if you used a variable that was simply default-initialized rather than having been given an explicit value.
>
> Yep. Exactly.
>
> I would love to opt-in such a feature via e.g.:
>
> struct Handle {
>     ...
>    @disable Handle.init;
>    ...
> }

If you have

@disable this();

in your struct, then that makes it illegal to default-initialize that type. However, it still has an init value (since _all_ types in D have init values), and so something like move would still use the init value, and any type introspection using init would continue to work. It's just the default initialization which is then illegal, forcing you to explicitly initialize all variables of that type.

That being said, I wouldn't advise disabling default initialization unless you really need to, because a number of things rely on it (e.g. I'm not sure if you can stick a type which can't be default-initialized into an array, because certain array operations depend on default initialization). So, while it's occasionally useful, it's one of those things that tends to cause problems when you do it.

- Jonathan M Davis