Producing nicer template errors in D libraries

H. S. Teoh hsteoh at quickfur.ath.cx
Tue Apr 10 12:45:41 PDT 2012


A lot of template code (e.g. a big part of Phobos) use signature
constraints, for example:

	void put(T,R)(R range, T data) if (isOutputRange!R) { ... }

This is all nice and good, except that when the user accidentally calls
.put on a non-range, you get reams and reams of compiler errors
complaining that certain templates don't match, certain other
instantiations failed, etc., etc.. Which is very unfriendly for newbies
who don't speak dmd's dialect of encrypted Klingon. (And even for
seasoned Star Trek^W^W I mean, D fans, it can take quite a few seconds
before the real cause of the problem is located.)

So I thought of a better way of doing it:

	void put(T,R)(R range, T data)
	{
		static if (isOutputRange!R)
		{
			... // original code
		}
		else
		{
			static assert(0, R.stringof ~ " is not an output range");
		}
	}

This produces far more readable error messages when an error happens.
But it also requires lots of boilerplate static if's for every function
that currently uses isOutputRange in their signature constraint.

So here's take #2:

	void put(T,R)(R range, T data) if (assertIsOutputRange!R) { ... }

	template assertIsOutputRange(R)
	{
		static if (isOutputRange!R)
			enum assertIsOutputRange = true;
		else
			static assert(0, R.stringof ~ " is not an output range");
	}

Now we can stick assertIsOutputRange everywhere there was a signature
constraint before, without needing to introduce lots of boilerplate
code.

But what if there are several overloads of the same function, each with
different signature constraints? For example:

	int func(T)(T arg) if (constraintA!T) { ... }
	int func(T)(T arg) if (constraintB!T) { ... }
	int func(T)(T arg) if (constraintC!T) { ... }

If constraintA asserts, then the compiler will not compile the code even
if the call actually matches constraintB.

So for cases like this, we introduce a catchall overload:

	int func(T)(T arg)
		if (!constraintA!T && !constraintB!T && !constraintC!T)
	{
		static assert(0, "func can't be used for type " ~
			T.stringof ~ " because <insert some excuse here>");
	}

Now the compiler will correctly resolve the template to instantiate,
while still providing a nice error message for when nothing matches.

What do y'all think of this idea?

(Personally I think it's really awesome that D allows you to customize
compiler errors using static assert, and we should be taking advantage
of it much more. I propose doing this at strategic places in Phobos,
esp. where you'd otherwise get errors from 5 levels deep inside some
obscure nested template that hardly anybody understands how it's related
to the original failing instantiation (e.g. a no-match error from
appendArrayWithElemImpl instantiated from appendToArrayImpl instantiated
from nativeArrayPutImpl instantiated from arrayPutImpl instantiated from
putImpl instantiated from put -- OK I made that up, but you get the
point).)


T

-- 
Amateurs built the Ark; professionals built the Titanic.


More information about the Digitalmars-d mailing list