Fake IFTI-compatible struct constructors

Chad Joan chadjoan at gmail.com
Sat May 1 21:57:54 UTC 2021


I came up with a couple techniques for making it seem like 
templated structs deduce their template parameters from 
constructor invocations. These are given further down in the post.

This has been a profitable exercise. However, the techniques I 
came up with have some drawbacks.

Thus, I have some questions for the community:

Is there a better way?
Do any of you have other constructor-IFTI-faking techniques to 
share?

Maybe one of you has encountered this already and come up 
different ways to approach this. Please share if you can!

### Background:

I ended up distracted with trying to make IFTI (Implicit Function 
Template Instantiation) work for a struct constructor.

I'm already aware of the normal factory function technique (ex: 
have `foo(T)(T x)` construct `struct Foo(T)`) and have looked 
into some of the history of this issue, like this thread from 
April of 2013:
https://forum.dlang.org/thread/lkyjyhmirazaonbvfyha@forum.dlang.org

I updated to DMD v2.096.1 to see if IFTI for constructors had 
been implemented recently.

But it seems not, because code like this fails to compile:

```D
struct Foo(StrT)
{
	StrT payload;
	this(StrT text)
	{
		this.payload = text;
	}
}

void main()
{
	import std.stdio;
	auto foo = Foo("x");
	writeln(foo.payload);
}
```

Compilation results:
```
xfail.d(13): Error: struct `xfail.Foo` cannot deduce function 
from argument types `!()(string)`, candidates are:
xfail.d(1):        `Foo(StrT)`
```

What followed was a series of experiments to see if I could get 
most of what I wanted by (ab)using existing D features.

### Technique 1: Mutually Exclusive Constraints

This technique was the first one I came up with.

It's disadvantage is that it only works for templated types with 
parameter lists that meet specific criteria:
* At least one of the parameters must be a string or array type.
* There can't be any other versions of the template that take the 
element's type at the same position.

So if I have a templated struct that parameterizes on string 
types, then this works for that.

The advantage of this method (over the later method I discovered) 
is that it doesn't regress the long or free-standing (non-IFTI) 
form of template instantiation in any way that I've noticed. As 
long as the above parameter list criteria can be met, it's 
strictly an upgrade.

It looks like this:

```D
// Fake IFTI constructor for templates with at least one string 
parameter.
auto Foo(Char)(Char[] text)
	// The constraint basically just ensures
	// that `Char` isn't a string or array.
	if ( !is(Char T : T[]) )
{
	import std.stdio;
	Foo!(Char[]) foo;
	foo.payload = text;
	pragma(msg, "function Char.stringof == "~Char.stringof);
	return foo;
}

struct Foo(StrT)
	// This constraint ensures the opposite:
	//   that `StrT` is a string|array.
	// It also declares `CharT` as it's
	//   element type, but that's quickly discarded.
	if ( is(StrT CharT : CharT[]) )
{
	StrT payload;
	pragma(msg, "struct StrT.stringof  == " ~ StrT.stringof);

	/+
	// These fail to compile.
	// Presumably, `CharT` isn't declared /in this scope/.
	CharT ch;
	pragma(msg, "struct CharT.stringof == " ~ CharT.stringof);
	// (It's not a big deal though. It's easy enough
	// to get the element type from an array without
	// the help of the template's parameter list.)
	+/
}

void main()
{
	import std.stdio;

	// Test IFTI-based instantiation.
	auto foo1 = Foo("1");  // IFTI for Foo constructor!
	writeln(foo1.payload); // Prints: 1

	// Test normal instantiation.
	// (In other words: we try to avoid regressing this case.)
	Foo!string foo2;
	foo2.payload = "2";
	writeln(foo2.payload); // Prints: 2

	/// Accidental instantiations of the wrong template are
	/// prevented by the `!is(Char T : T[])` template constraint.
	// Foo!char foo3; // Error: `Foo!char` is used as a type
	// foo3.payload = "3";
}
```

### Technique 2: Explicit Instantiation

I wanted a way to do this with templates that don't deal with 
strings or arrays, and with templates that might accept either an 
array or its element in the same parameter (or parameter 
position).

So I did that, but it has a notable drawback: free-standing 
instantiations like `Bar!int bar;` won't work without adding 
additional template specializations for every type that might 
need to be used that way.

This drawback isn't very severe for project internals, but I 
think it's no good for library APIs.

I really hope someone has a way to do something like this, but 
without this drawback.

Here's what this technique looks like:

```D
// This is like the usual helper function that returns
// an instance of the desired templated type.
// (AKA: a factory function)
//
// HOWEVER, unlike normal factory functions, this one
// has the same name as the template+type that it returns.
// This is what IFTI on constructors would give us,
// but D doesn't have that yet (v2.096.1).
// This function allows us to fake it.
//
auto Bar(T)(T val)
{
	Bar!(T, 0) bar;
	bar.payload = val;
	pragma(msg, "function T.stringof == "~T.stringof);
	return bar;
}

// Below is a normal template-struct, with the exception
// of an extra `int x = 0` parameter. This parameter
// distinguishes it from the above function-template
// (factory function) of the same name.
//
// This makes it necessary to explicitly instantiate
// this version of the template by passing a value for
// the `x` parameter (it doesn't matter what).
//
// The explicit instantiation is good and bad.
// The good is that it allows this setup to work at all.
// The bad is that free-standing instantiations of the
// expected `Bar` template (ex: `Bar!string  bar`) will
// now try to pick the function-template instead of this one.
//
// We will try to address the shortcoming further down,
// by using tighter template specializations to intercept
// free-standing instantiations.
//
struct Bar(T, int x = 0)
{
	T payload;
	pragma(msg, "struct T.stringof  == " ~ T.stringof);
}

// Below is the workaround for (re)allowing
// free-standing instantiations.
//
// This part sucks. It's the weakness of this technique.
//
// For now, I can make free-standing instantiations work on
// a case-by-case basis by using "specialized" templates
// to direct the compiler away from the (less specialized)
// function-template.
//
// Presumably, something like `Bar("x")` won't search these
// because the function implementing it is not declared
// in any of these more specialized templates.
//
// This makes me wish I had a better way to do this.
//
template Bar(T : float)  { alias Bar = Bar!(T, 0); }
template Bar(T : int)    { alias Bar = Bar!(T, 0); }
template Bar(T : string) { alias Bar = Bar!(T, 0); }
template Bar(T : char)   { alias Bar = Bar!(T, 0); }
template Bar(T : Qux)    { alias Bar = Bar!(T, 0); }

// Testing of user-defined types.
struct Qux
{
	int x;
	string toString() { return "Qux"; }
}

// --------
void main()
{
	import std.stdio;

	Qux qux;

	// Test IFTI-based instantiation.
	auto bar1 = Bar(42);  // IFTI for Foo constructor!
	auto bar2 = Bar("s"); // And for any type, too!
	auto bar3 = Bar('c');
	auto bar4 = Bar(0.7f);
	auto bar5 = Bar(qux);
	writefln("%d", bar1.payload); // 42
	writefln("%s", bar2.payload); // s
	writefln("%c", bar3.payload); // c
	writefln("%f", bar4.payload); // 0.700000
	writefln("%s", bar5.payload); // Qux

	// Test normal instantiation.
	// (In other words: we try to avoid regressing this case.)
	Bar!int barA;
	barA.payload = 42;
	writefln("%d", barA.payload); // 42

	Bar!string barB;
	barB.payload = "s";
	writefln("%s", barB.payload); // s

	Bar!char barC;
	barC.payload = 'c';
	writefln("%c", barC.payload); // c

	Bar!float barD;
	barD.payload = 0.7f;
	writefln("%f", barD.payload); // 0.700000

	Bar!Qux barE;
	barE.payload = qux;
	writefln("%s", barE.payload); // Qux
}
```

### A Failed Attempt
(But a notable one.)

The previous technique involved exploiting template 
specialization tiers to allow templates with very similar 
parameter lists to exist side-by-side. At that point, a function 
call causes the compiler to search the less specialized version 
instead of the more specialized version. On the other hand, a 
type declaration allows it to search both versions, but due to 
how specialization works, the compiler picks the more specialized 
version in that case. Hence it is possible to distinguish between 
the factory-function case and the just-a-template case. At least, 
that's how I understand it.

Given that hypothesis, I decided to try shifting the whole system 
by one specialization tier, because `alias T` is less specialized 
than `T` (or, at least, that's how I read the spec). This would 
make things of the form `T : int`, `T : float`, `T : string`, 
etc, no longer necessary, and the drawback of technique #2 
should, in principle, disappear.

However, this was not to be. Rather, this setup doesn't work at 
all, because IFTI doesn't seem to work on function-templates with 
alias parameters. Or maybe it's something else; I've probably 
spent too much time thinking about it anyways.

Specifically, my attempts took this form:

```D
// Use "alias" parameter to make
// this template be less... "specialized".
auto Bar(alias T)(T val)
{
	Bar!(T, 0) bar;
	bar.payload = val;
	pragma(msg, "function T.stringof == "~T.stringof);
	return bar;
}

// In principle, this should be just as specialized
// as the previous function-template, but also have
// the additional (int) value parameter that makes
// it require explicit instantiation.
// (I also tried `int x` instead of `int x = 0`,
// and also tried `T` instead of `alias T`. The
// `int x` might be the more important feature anyways.)
struct Bar(alias T, int x = 0)
{
	T payload;
	pragma(msg, "struct T.stringof  == " ~ T.stringof);
}

// Because the function-template is less specialized
// than this one, this one should be preferred by the
// compiler when free-standing instantiation is performed.
template Bar(T)  { alias Bar = Bar!(T, 0); }

// ...

// --------
void main()
{
	// ...
}

```

This, and related permutations I've tried, tend to give error 
messages like so:
```
alias_test.d(130): Error: template `alias_test.Bar` cannot deduce 
function from argument types `!()(int)`, candidates are:
alias_test.d(3):        `Bar(alias T)(T val)`
alias_test.d(15):        `Bar(alias T, int x)`
alias_test.d(24):        `Bar(T)`
alias_test.d(131): Error: template `alias_test.Bar` cannot deduce 
function from argument types `!()(string)`, candidates are:
alias_test.d(3):        `Bar(alias T)(T val)`
alias_test.d(15):        `Bar(alias T, int x)`
alias_test.d(24):        `Bar(T)`
... and so on ...
```

So it's not as easy as just gaming the specialization tiers. Or 
perhaps it is, but there's some oddly specific hangup, like IFTI 
failing to invoke for function-templates with alias parameters.


More information about the Digitalmars-d-learn mailing list