Skip to content

Add std.typecons.Rebindable!struct#4363

Closed
ntrel wants to merge 13 commits intodlang:masterfrom
ntrel:rebind-struct
Closed

Add std.typecons.Rebindable!struct#4363
ntrel wants to merge 13 commits intodlang:masterfrom
ntrel:rebind-struct

Conversation

@ntrel
Copy link
Copy Markdown
Contributor

@ntrel ntrel commented May 25, 2016

Rebindable!Struct:
"Models safe reassignment of otherwise constant struct instances.

A constant struct with a field of reference type cannot be assigned to a mutable
struct of the same type. This protects the constant reference field from being
mutably aliased, potentially allowing mutation of immutable data. However, the
assignment could be safe if all reference fields are only exposed as const.

Rebindable!(const S) accepts assignment from
a const S while enforcing only constant access to its fields.
Rebindable!(immutable S) does the same but field access may create a
temporary copy of S in order to enforce true immutability."

@Hackerpilot
Copy link
Copy Markdown
Contributor

Unit tests fail.

@ntrel
Copy link
Copy Markdown
Contributor Author

ntrel commented May 25, 2016

@Hackerpilot Thanks. At least one All tests pass now.

std/typecons.d Outdated
this()(S s) @trusted
{
// we preserve tail immutable guarantees so cast is OK
payload = cast(Unqual!S)s;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we preserve tail immutable guarantees so cast is OK

Are you sure? Casting away const is UB in any situation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Casting away const is UB in any situation.

Just casting is ok. You just can't mutate through the casted reference.

See https://dlang.org/spec/const3.html#removing_with_cast

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the payload has an opAssign, it will be able to mutate through it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, maybe that's why existing Rebindable uses a union to get around the problem.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@schuetzm I've changed the implementation to use a union now, I think that solves the mutable opAssign problem. (I'm not entirely sure about the destructor, I'll try to do some tests).

@ntrel
Copy link
Copy Markdown
Contributor Author

ntrel commented May 30, 2016

@Hackerpilot can you remove (or explain) the needs work label please ;-) All unit tests currently pass.

std/typecons.d Outdated
S payload;
}

@trusted:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can this be true when S can have an arbitrarily unsafe opAssign or dtor?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's OK now, opAssign is never called, and S.~this has its safety inferred. Rebindable.thisalso uses inferrence for safety of S.this.

@dnadlinger
Copy link
Copy Markdown
Contributor

(keeping the "needs work" label up to date is impossible for people without commit access, so removing it for now)

@ntrel
Copy link
Copy Markdown
Contributor Author

ntrel commented May 30, 2016

Also there's another problem: unions can't have struct members that themselves define destructors or postblits.

Edit: I'll try to fix this using void[] payload in the next few days.

std/typecons.d Outdated

void opAssign(S s) @trusted
{
auto rs = Rebindable(s);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I'm missing something, this runs S's postblit which may be unsafe, breaking opAssign's @trusted promise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, should be fixed now.

Unions don't allow struct members which have postblit or dtors.
@ntrel
Copy link
Copy Markdown
Contributor Author

ntrel commented Jun 6, 2016

Implemented changing union -> void[].

@ntrel ntrel mentioned this pull request Jun 28, 2016
@ntrel
Copy link
Copy Markdown
Contributor Author

ntrel commented Jun 28, 2016

Ping?

This is part of making Rebindable generic (which should be simple once this is this merged). This will help to implement functions like std.algorithm.searching.extremum correctly - instead of using Unqual for extremeElement we should use Rebindable. I expect this goes for various other places in Phobos too.

Also, should this go in std.experimental first?

Copy link
Copy Markdown
Contributor

@wilzbach wilzbach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of first impressions


/// ditto
Rebindable!S rebindable(S)(S s)
if (is(S == struct) && !isInstanceOf!(Rebindable, S))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Phobos style is to avoid such type overloads and hide them as implementation details.

() @trusted {emplace!S(mutPayload, s);}();
else
emplace!S(mutPayload, s);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about pushing this @safe improvement to std.conv.emplace?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's only safe if S.~this is run when mutPayload goes out of scope, which emplace can't enforce.


// immutable S cannot be passed to auto ref S above
static if (!is(S == immutable))
this()(immutable S s)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's what inout was made for ;-) -> inout auto ref S s

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't work. I'm a bit rusty on this PR but I remember struggling with compile errors - inout triggers them.

static if (!is(S == immutable))
void opAssign()(immutable S s)
{
emplace(s);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is there no movePayload (and thus call to of destructor of the internal payload)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, thanks.

}

static if (!is(S == immutable))
ref S Rebindable_getRef() @property
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be private

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't be because it's used for alias this.

}

static if (is(S == immutable))
S Rebindable_get() @property
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto (private)

* temporary copy of `S` in order to enforce _true immutability.
*/
template Rebindable(S)
if (is(S == struct))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be awesome if you could put all Rebindable types under this template as sth. like __traits(isSame, TemplateOf!(typeof(ri2)), Rebindable)
would work. Currently Rebindable references to the first symbol - the one for classes etc.

emplace(s);
}

void opAssign()(auto ref S s)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for

Rebindable!(immutable S) ri = S(new int);
auto ri2 = ri;

this yields conflicting overloads.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me - I added a test for this, let's see if the autotester passes.

std/typecons.d Outdated
~this()
{
// call destructor with proper constness
S s = movePayload;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove the S s = part ;-)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@wilzbach
Copy link
Copy Markdown
Contributor

Also, should this go in std.experimental first?

While usually it's not needed, it seems that your PR got quite stalled and for std.experimental the barrier is lower, so it might be a good option to push your PR forward.

I made a simple test for your code to check the lifetime by counting postblit and destructor calls.
It shows the effect of the temporary copies that get created on every field access and it was also a good test to realize that one of your opAssign was missing the destructor call. Maybe you could include sth. like this to your tests?
Note that atm also you can't do the following auto ri2 = ri; due to overload conflicts.

Last but not least

  • if you rebase this PR to upstream/master, we would be supplied with coverage annotations.
  • you now can directly add a changelog entry to your PR (I think that this PR needs one)
unittest
{
    import std.stdio;
    int del;
    int post;
    struct S
    {
        int* ptr;
        int level;
        this(this) {
            writeln("S:this(this)");
            post++;
            level++;
        }
        ~this() {
            writeln("S:~this");
            del++;
        }
    }

    // test const
    {
        Rebindable!(const S) rc = S(new int);
        assert(post == 1);
        assert(rc.level == 1);
        assert(post == 1);
        assert(rc.level == 1); // no copy is created
    }
    assert(post == 1);
    assert(del == 2);
    del = 0, post = 0;

    // immutable
    {
        // on every field access a temporary immutable copy gets created
        Rebindable!(immutable S) ri = S(new int);
        assert(post == 1);
        assert(ri.level == 2);
        assert(post == 2);
        assert(ri.level == 2); // however it's a copy of the payload's level
    }
    assert(post == 3);
    assert(del == 4);
    del = 0, post = 0;

    {
        // the initial value is copied and destructed
        Rebindable!(const S) rc = S(new int);
        assert(post == 1);
        assert(del == 1);
        assert(rc.level == 1);

        // on an assignment to another value is simply post-blitted
        const S cs = rc;
        assert(del == 1);
        assert(post == 2);
        assert(rc.level == 1);
        assert(cs.level == 2);

        // however on an assignment, the old payload gets destructed
        rc = cs;
        assert(del == 2);
        assert(post == 3);
        assert(cs.level == 2);
        assert(rc.level == 3);
    }
    assert(post == 3);
    assert(del == 4);
    del = 0, post = 0;

    {
        // the initial value is copied and destructed
        Rebindable!(immutable S) ri = S(new int);
        assert(post == 1);
        assert(del == 1);
        // creates temporary (gets immediately destroyed), actual level is still 1
        assert(ri.level == 2);
        assert(del == 2);
        assert(post == 2);

        // on an assignment to another value is simply post-blitted
        const S cs = ri;
        assert(del == 2);
        assert(post == 3);
        // creates temporary (gets immediately destroyed), actual level is still 1
        assert(ri.level == 2);
        assert(cs.level == 2);
        assert(del == 3);
        assert(post == 4);

        // however, on an assignment to Rebindable the old value gets destructed
        auto ri2 = ri;
        static assert(is(typeof(ri2) == Rebindable!(immutable S)));
        static assert(TemplateOf!(typeof(ri2)).stringof == "Rebindable(S) if (is(S == struct))");
        assert(del == 3);
        assert(post == 4);
        assert(ri2.level == 2); // should be 3??
        assert(del == 4);

        // similarly an assignment
        //ri = ri2; // fails to compile due to overload conflicts
    }
    assert(post == 5);
    assert(del == 7);
}

@ntrel
Copy link
Copy Markdown
Contributor Author

ntrel commented Feb 21, 2017

@wilzbach Thanks for reviewing this! I'm updating my toolchain so I can work against the latest master. Also I will move the changes to std.experimental, which will deal (for now) with the separate Rebindable template.

@wilzbach wilzbach added this to the 2.075.0 milestone Mar 4, 2017
@MartinNowak MartinNowak removed this from the 2.075.0 milestone Jun 25, 2017
@wilzbach
Copy link
Copy Markdown
Contributor

wilzbach commented Jul 9, 2017

Also I will move the changes to std.experimental, which will deal (for now) with the separate Rebindable template.

@ntrel Did you ever manage to find time for this?

@MetaLang
Copy link
Copy Markdown
Member

MetaLang commented Aug 6, 2017

It's been a year so it looks like this is pretty much dead. I'm going to close this but @ntrel please comment if you want me to re-open it.

@wilzbach
Copy link
Copy Markdown
Contributor

wilzbach commented Feb 7, 2018

Hmm, I still think this is quite useful (I have had the need for it a few times over the last months), so I have revived this PR to #6136

(I have already addressed all my comments, except for the opAssign overload conflict)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.