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.