implicit conversion

H. S. Teoh via Digitalmars-d-learn digitalmars-d-learn at puremagic.com
Tue Aug 12 12:02:18 PDT 2014


On Tue, Aug 12, 2014 at 04:04:41PM +0000, Jonathan M Davis via Digitalmars-d-learn wrote:
> On Tuesday, 12 August 2014 at 15:39:09 UTC, Meta wrote:
> >What I mean is that this breaks the Liskov Substitution
> >Principle, which alias this should obey, as it denotes a subtype.
> >Since S!float has an alias this to float, it should behave as a
> >float in all circumstances where a float is expected; otherwise,
> >we've got a big problem with alias this on our hands.
> 
> IMHO, it was a mistake to add alias this to the language. It's
> occasionally useful, but it's too dangerous.

I disagree. Alias this is a very powerful tool that we haven't fully
leveraged yet. Not only it allows for implicit conversion to native
types (which makes it very useful for transparent type wrapping and
user-defined numeric types that are on par with built-in types), it also
allows for things like transparent safe dereference handling, as I
demonstrated some time ago.


> Implicit conversions wreak havoc with templates, because inevitably
> what happens is that a type is tested for whether it implicitly
> converts to a particular type, but then the template is instantiated
> with the original type, not the implicitly converted one, and then the
> template frequently fails to compile - or if it does compile, it may
> do weird things, because you're dealing with a type that doesn't act
> as expected.

That's because the template code was wrongly written. If it expects the
basic type, then it should be constrained to take only basic types
(is(T==int) instead of is(T : int)). If it wants a member variable to be
a basic type, then it should use the converted-to type instead of the
original type (if it tests is(T : int) but wants an int, then it should
use int as the target type, not T).

Problems with carelessly-written template code is no fault of alias
this.


> If you're dealing with a template which doesn't accept implicit
> conversions (e.g. isNaN), and the implicit conversion were tested
> after the actual type failed, and the template was then
> instantiated with the implicitly converted type, then maybe that
> could work, but that's not how it works now, and in general, I
> think alias this is just too dangerous to use.
[...]

I disagree. If a template function can take an implicitly-converted
type, then it should assign the original type to the target type if
that's what it wants to use. For example:

	auto badCode(T)(T t)
		if (is(T : float))
	{
		T u = 1.0; // does anyone expect this to NOT blow up?
		u += t;    // or this?
		...
	}

	struct S {
		float toFloat() { ... }
		alias toFloat this;
	}
	auto r = badCode(S.init); // oops

	auto goodCode(T)(T t)
		if (is(T : float))
	{
		float tf = t; // now we're talking
		float u = 1.0; // now this for sure won't blow up
		u += tf;
		...
	}
	auto s = goodCode(S.init); // OK

Basically, the root of the problem is that the way C++ and D templates
work, the template body is free to assume *anything* about the incoming
types, with or without verification (mostly without, IME). As a result,
template code often makes unfounded assumptions, like:

	auto myFunc(T)(T t)
		if (is(T : float))
	{
		float f = t; // this is about the only safe thing we can
			     // do, given the stated assumptions
		...
		T u; // are we sure the type is instantiable?
		u = 1.0; // are we sure the type accepts literal assignment?
		auto r = t + 1.0; // are we sure the result type is anything we expect?
		...
		// etc.
	}

This example is blatantly obvious, but in practice, things are a lot
more tricky. Classic example:

	auto myFunc(R)(R range)
		if (isInputRange!R)
	{
		// what makes us think typeof(range.front) is assignable?
		auto f = range.front;
		...
		range.popFront();

		// This fails on transient ranges, but happily compiles
		// 'cos isInputRange doesn't ensure the assumption that
		// assigned values of .front persist beyond .popFront.
		// Basically, it's an unfounded assumption.
		if (f == range.front) { ... }
	}

Another typical example of poorly-written template code:

	auto nextColumn(RoR)(RoR rangeOfRanges)
		if (isForwardRange!RoR && isInputRange!(ElementType!RoR))
	{
		foreach (subrange; rangeOfRanges)
		{
			// what makes us think this has any persistent
			// effect past this iteration of the loop?
			subrange.popFront();
		}

		// For all we know, this could just be returning the
		// original, unmodified range, because
		// rangeOfRanges.front could be returning temporary
		// copies of the actual subranges.
		//
		// Or it could return an empty range because we forgot
		// to call .save. But even if we *did* call .save, what
		// makes us think .popFront() on the subranges has any
		// persistent effect on either the original *or* the
		// .save'd range outside the foreach loop?
		return rangeOfRanges;
	}

The problem is, neither isForwardRange!RoR nor
isInputRange!(ElementType!RoR) grant us any foundation at all for the
assumptions that the code makes. True, some of these assumptions cannot
be expressed as sig constraints, but they should at least be documented
up front in bold. Unfortunately, most of the docs I see for such
functions are underdocumented, and generally do not state any such
assumptions.

Correctly-written template code would explicitly test for all properties
it wishes to use, and make no blind assumptions about anything. For
example:

	auto goodCode(T)(T t)
		if (is(T : float)
			&& is(typeof({T t;})) // ensure T is instantiable!
			&& is(typeof(T.init < T.init)) // ensure T is comparable
			&& is(typeof(t = t)) // ensure T is assignable
			&& ... /* etc */)
	{
		T u;
		if (u < t) { ... }
		u = t;

		float f = t; // N.B.: *not* "auto f = t"
		... // etc.
	}

	auto badCode(T)(T t)
		if (is(T : float))
	{
		float tmp = t + 1.0; // who says the result of + is a float?
		auto tmp2 = t + 1.0; // this seems to work...
		T u = tmp2;	// how do we know this is valid?
		if (u < t) ...	// how do we know T is comparable?
			t = u;	// how do we know T is assignable?

		if (u < u.max)	// how do we know T.max exists?
			...

		int x = u.ndig;	// how do we know T.ndig == cast(float)t.ndig?
		...
		// etc.
	}

tl;dr: there are so many ways template code can go wrong, that I don't
it justifies blaming alias this for problems.


T

-- 
Why ask rhetorical questions? -- JC


More information about the Digitalmars-d-learn mailing list