Thread overview
Fake IFTI-compatible struct constructors
May 01, 2021
Chad Joan
May 01, 2021
Basile B.
May 02, 2021
Chad Joan
May 01, 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:

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:

// 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:

// 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:

// 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.

May 01, 2021

On Saturday, 1 May 2021 at 21:57:54 UTC, Chad Joan wrote:

>

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.
...

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.

Yes your observation is correct. That should work but this is currently not implemented and referenceced as issue 20877

May 02, 2021

On Saturday, 1 May 2021 at 23:21:33 UTC, Basile B. wrote:

>

On Saturday, 1 May 2021 at 21:57:54 UTC, Chad Joan wrote:

>

... Rather, this setup doesn't work at all, because IFTI doesn't seem to work on function-templates with alias parameters.

Yes your observation is correct. That should work but this is currently not implemented and referenceced as issue 20877

Good to know. Thank you!