What are some ways to get more strict type-checking?

Adam D. Ruppe destructionator at gmail.com
Mon May 6 14:41:31 UTC 2019


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.


More information about the Digitalmars-d-learn mailing list