On Monday, 6 May 2019 at 02:02:52 UTC, Devin wrote:
But to my astonishment, the broken code compiled without any warnings or notifications.

Yeah, I kinda wish bool (and char too, while we're at it) wouldn't implicitly convert to int.

alias ID = uint;

Since this is an alias, there is zero difference between this and uint, so you inherit its quirks...


The compiler interprets the attempted assignment as calling the constructor with one argument, and then converts the boolean to a uint to match the function overload. So in effect, my struct is implicitly convertible from a bool.

I need to correct this to make sure we are on the same page for vocabulary: it is *explicitly* constructed here, it just happens to share the = syntax with assignment... but since it is a new variable being declared here, with its type given, this is explicit construction.

And this construction can occur in a `a = x;` context too, without a declaration, if it happens in an aggregate constructor.

MyStruct a = x; // explicit construction, but with = syntax
class A {
   MyStruct a;
   this() {
       a = x; // considered explicit construction!
   }
}

But:

void foo(MyStruct a) {}

foo(MyStruct(x)); // explicit construction

foo(not_a_struct); // this is implicit construction, and banned by D


And meanwhile:

MyStruct a;

a = x; // now this is assignment

class A {
   MyStruct a;
   void foo() {
       a = x; // this is also assignment
   }
}


The syntax needs to be taken in context to know if it is assignment or construction.

If it is construction, it calls this(rhs) {} function, if assignment, it calls opAssign(rhs) {} function.


But, once the compiler has decided to call that function, it will allow implicit conversion to its arguments. And that's what you saw: implicit conversion to the necessary type for an explicit construction.


So, it is the implicit conversion to our type we want to prohibit. But, remember that implicit construction, the function call thing we mentioned thing, is banned. Which brings us to a potential solution.

* Change "ID" from an alias to a struct of some sort. I've been trying to find similar issues, and I saw once suggested that a person could make a struct with one member and conversions to and from different types. I've also seen "alias X this;" a lot. But my main issues is stopping these conversions, and everything I've seen is about enabling automatic conversion. Ideally, I would have something that's convertible TO a uint when needed, but can't be converted FROM other data types.

This is your answer (though keep reading, I do present another option at the end of this email too that you might like).

struct ID {
    uint handle;
}

And then, if you must allow it to convert to uint, do:

struct ID {
    uint handle;
    alias handle this;
    // and then optionally disable other functions
    // since the alias this now enables ALL uint ops...
}

or if you want it to only be visible as a uint, but not modifiable as one:

struct ID {
   private uint handle_;
   @property uint handle() { return handle_; }
   alias handle this; // now aliased to a property getter
   // so it won't allow modification through that/
}


Which is probably the best medium of what you want.



Let's talk about why this works. Remember my example before:


void foo(MyStruct a) {}
foo(MyStruct(x)); // explicit construction
foo(not_a_struct); // this is implicit construction, and banned by D

And what the construction is rewritten into:

MyStruct a = x; // becomes auto a = MyStruct.this(x);


alias this works as implicit conversion *from* the struct to the thing. Specifically, given:

MyStruct a;

If, `a.something` does NOT compile, then it is rewritten into `a.alias_this.something` instead, and if that compiles, that code is generated: it just sticks the alias_this member in the middle automatically.

It will *only* ever do this if: 1) you already have an existing MyStruct and 2) something will not automatically work with MyStruct directly, but will work with MyStruct.alias_this.

#1 is very important: alias this is not used for construction, in any of the forms I described above. It may be used for assignment, but remember, not all uses of = are considered assignment.


Let's go back to your code, but using a struct instead.

---

struct ID {
    uint handle_;
    @property uint handle() { return handle_; }
    alias handle this;
}

struct Data
{
        ID id;
        this(ID id)
        {
                this.id = id;
        }
}

// Forgot to refactor a function to return its
// loaded data, rather than a success/fail bool
bool load_data()
{
        // some processing
        return true;
}

int main()
{
        // Very obviously a bug,
        // but it still compiles
        Data d = load_data();
        return 0;
}

---

kk.d(31): Error: constructor kk.Data.this(ID id) is not callable using argument types (bool) kk.d(31): cannot pass argument load_data() of type bool to parameter ID id


Yay, an error! What happens here?

Data d = load_data();

rewritten into

Data d = Data.this(load_data() /* of type bool */);

Data.this requires an ID struct... but D doesn't do implicit construction for a function arg, so it doesn't even look at the alias this. All good.

What if you *wanted* an ID from that bool?

        Data d = ID(load_data());

That compiles, since you are now explicitly constructing it, and it does the bool -> uint thing. But meh, you said ID() so you should expect that. But, what if we want something even more exact? Let's make a constructor for ID.

This might be the answer you want without the other struct too, since you can put this anywhere to get very struct. Behold:

struct ID {
    uint handle_;

    @disable this(U)(U u);
    this(U : U)(U u) if(is(U == uint)) {
       handle_ = u;
    }

    @property uint handle() { return handle_; }
    alias handle this;
}


That stuff in the middle is new. First, it disables generic constructors.

Then it enables one specialized on itself and uses template constraints - which work strictly on the input, with no implicit conversion at all (unless you want it - is(U : item) allows implicit conversion there and you can filter through).

Now you can get quite strict. Given that ID:

        Data d = ID(load_data()); // will not compile!
        Data d2 = ID(0u); // this one will
        Data d3 = ID(0); // will not compile!

The difference between 2 and 3 is just that `u`... it is strict even on signed vs unsigned for these calls.

(thanks to Walter for this general pattern. to learn more, I wrote about this more back in 2016: http://arsdnet.net/this-week-in-d/2016-sep-04.html )



There's a lot of options here, lots of control if you want to write your own structs and a bit more code to disable stuff.

Reply via email to