__traits(compiles, ...) with syntax errors

H. S. Teoh hsteoh at quickfur.ath.cx
Mon Jan 14 23:58:59 UTC 2019


On Mon, Jan 14, 2019 at 04:31:06PM -0700, Jonathan M Davis via Digitalmars-d wrote:
> On Monday, January 14, 2019 2:57:28 PM MST Paul Backus via Digitalmars-d 
> wrote:
> > On Monday, 14 January 2019 at 21:47:40 UTC, Steven Schveighoffer
> >
> > wrote:
> > > How many times have you written something like:
> > >
> > > void foo(T)(T t) if (__traits(compiles, t.bar())) // or
> > > is(typeof(t.bar()))
> > > {
> > >
> > >    t.bar();
> > >
> > > }
> > >
> > > And somehow, somewhere, this isn't called? Then you remove the
> > > constraint, and find that there's a syntax error in the bar
> > > template function (like a missing semicolon).

Yeah, I've run into this problem far too many times.  It's extremely
annoying because the error is silent, and makes the compiler just move
on to a different overload, leaving you scratching your head as to why
it's not picking the "correct" overload.


> > Ideally, you would catch this in the unit tests for `bar`. You are
> > writing unit tests, aren't you? ;)
[...]

The problem is not just trivial syntax errors like Steven's example.
The problem is that sufficiently complex sig constraints will sometimes
have unexpected corner cases that works in general, but fails for
specific cases.

One nasty example that comes to mind is when the sig constraint only
works with mutable types (or some other such qualified type) because you
used an overly-restrictive test.  Initial unittests work because you're
not expecting that changing the input to const would cause any trouble.
Then some time later, you happen to need to pass a const object, and
suddenly the compiler seemingly refuses to acknowledge that this
overload of the template function exists, and insists on trying a
different overload, or failing with a totally unhelpful error saying no
overloads match.

If you're dealing with something with as many overloads as
std.conv.toImpl, for example, good luck finding what went wrong. You
first have to parse every single darned sig constraint on every single
overload, just to find the one overload that *should* match, but
doesn't.  Then you have to parse the sig constraint clause by clause
just to determine which clause failed.  And *then*, if you're really
unlucky and the clause is a complex one (that calls another template,
say, like isXyzRange or something equally complex), *then* you have to
find the definition of that helper template, then parse through every
clause in *that* to find out what went wrong.  And if you're *truly*
unlucky, the helper template is of the form:

	isBlahBlahBlah(T) = __traits(compiles, (T t) {
		... // arbitrarily complex code here
	});

then you have to copy-n-paste the stupid lambda and instantiate it with
your exact version of T (and that's if the original template function's
sig constraint hadn't munged it with things like Unqual or any of the
other type-changing templates -- then good luck figuring out what T is
for your particular function call), just to get the stupid compiler to
tell you the stupid error that made it fail, because by default all
errors are gagged inside template instantiations during IFTI, so there's
nothing at all that even hints at what went wrong.

I've had to do this so many times when working with template-heavy code,
that recently I've started to develop a distaste for sig constraints.
Unless I'm working with complex overload sets where sig constraints are
unavoidable, I'd rather just leave the function without any sig
constraints -- then at least when something fails, you've a fighting
chance of knowing what went wrong without going through the above dance
*every* *single* *time*, because the compiler will now actually tell you
what the lousy compile error was.

And if you're really cursed, you'd run into a case like Jonathan says,
where there's a static if inside the template function that goes to the
else clause because it doesn't like your template argument for whatever
reason, so you just got switched to a different implementation with not
a clue as to how it ended up there.  Given enough nested static if's,
good luck figuring out just which one of them had the failing condition
that sent you down the wrong rabbit hole -- the failure is completely
silent; and remember, if the else-branch of the static if fails to
compile, the compiler will just gag the error and move on to the next
overload at the top level.  Good luck finding that one static if that
failed.

(Oh, and did I mention, almost all of the above requires you to edit the
*callee* code in order to find out where the template instantiation
went? Good luck if it goes into some common templates used everywhere
else, that if you change one line, something else unrelated in your
program will stop compiling so you won't even get to the point of
failure in the first place. Sometimes the only solution here is to
copy-n-paste the entire lousy template function into a different module
(possibly along with most/all of its dependencies) and call *that*
instead, so that the compiler won't prematurely die in unrelated code.)

This is a very real problem, and an extremely annoying one.  In a
hair-pullingly frustrating manner.  And of course, it makes you feel
*really* good 2 hours later after all of this jazz to discover that the
error came from a simple typo.


T

-- 
Life is too short to run proprietary software. -- Bdale Garbee


More information about the Digitalmars-d mailing list