www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - What are some ways to get more strict type-checking?

reply Devin <lee.hastings outlook.com> writes:
Recently, I poorly refactored some code, which introduced an 
obvious bug.  But to my astonishment, the broken code compiled 
without any warnings or notifications.  A minimum example is 
shown below:

alias ID = uint;
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;
}


So I'm assigning a boolean value to a struct with one uint field. 
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.

My question is basically... how do I make this not compile?  What 
methods do I have to prevent this sort of crazy implicit 
conversion?  I'm not familiar enough with the language to know my 
options.  I think there are a few:

* Enable more warnings.  DMD seems to only have "-w", which I 
believe is enabled through dub.  Are there more pedantic settings 
that would catch this conversion?

* 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.

* Replace the constructor with a static function.  This happened 
because D implicitly converted my assignment into a constructor 
call.  If I just don't have constructors, and instead define a 
static function to build instances of the struct, I could then 
have more control over assignment and avoid this more easily.


If anyone has other options, I would really want to hear them.  I 
know for a fact these types of issues are going to crop up in my 
project, and I want to nip them in the bud before the become 
harder to track down.
May 05 2019
next sibling parent sarn <sarn theartofmachinery.com> writes:
On Monday, 6 May 2019 at 02:02:52 UTC, Devin wrote:
 Recently, I poorly refactored some code, which introduced an 
 obvious bug.  But to my astonishment, the broken code compiled 
 without any warnings or notifications.  A minimum example is 
 shown below:

 alias ID = uint;
 ...
alias doesn't create a distinct type, but maybe Typedef from Phobos is what you want: https://dlang.org/library/std/typecons/typedef.html
May 05 2019
prev sibling parent reply Adam D. Ruppe <destructionator gmail.com> writes:
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. 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.
May 06 2019
next sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Mon, May 06, 2019 at 02:41:31PM +0000, Adam D. Ruppe via Digitalmars-d-learn
wrote:
 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.
[...] Yeah, we tried to change this recently but W&A shot it down with the argument that bool is to be understood as a 1-bit integer rather than an actual Boolean value that isn't tied to specific integer values. I disagree with that reasoning, but I don't think W&A are going to change their stance on that anytime in the foreseeable future. T -- "You know, maybe we don't *need* enemies." "Yeah, best friends are about all I can take." -- Calvin & Hobbes
May 06 2019
prev sibling parent reply Devin <lee.hastings outlook.com> writes:
On Monday, 6 May 2019 at 14:41:31 UTC, Adam D. Ruppe wrote:
 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/
 }
This seems like a good solution! I was aware that making an alias didn't actually enforce any type-checking, but I wasn't sure how to make something like this without it being really verbose.
May 07 2019
parent Devin <lee.hastings outlook.com> writes:
On Tuesday, 7 May 2019 at 13:46:55 UTC, Devin wrote:
 [snip]
I'm wrapping around OpenGL, which uses a int and uint for numerous types, so I decided to make a mixin template for making a sort of strict alias type. Importantly, they aren't assignable to each other, unlike Typedef. Here's the template I'm trying: mixin template StrictAlias(T) { private T _handle; disable this(U)(U u); this(U : U)(U data) if( is(U == T) ) { _handle = data; } property T handle() { return _handle; } alias handle this; } And here's how I'm using it: struct MaterialId { mixin StrictAlias!GLuint; } struct UniformId { mixin StrictAlias!GLint; } struct AttribId { mixin StrictAlias!GLint; } So now I can easily call OpenGL functions using values of these types, but I can't accidentally assign one to the other, and I can only construct them with exactly the type they alias. Thanks for the tips!
May 07 2019