Thread overview
What are some ways to get more strict type-checking?
May 06, 2019
Devin
May 06, 2019
sarn
May 06, 2019
Adam D. Ruppe
May 06, 2019
H. S. Teoh
May 07, 2019
Devin
May 07, 2019
Devin
May 06, 2019
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 06, 2019
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 06, 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.
May 06, 2019
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 07, 2019
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
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!