Reducing variadic template combinatorics (C++ was onto something)
Steven Schveighoffer
schveiguy at gmail.com
Tue Oct 14 04:30:49 UTC 2025
In a conversation on a pull request for one of my libraries, I
came across an interesting revelation. Variadic template
functions waste resources for the most part, because much of the
time, you don't care about the *relationship* between the
parameters. In many such functions, you just process them in a
loop.
Let's start with the traditional variadic D pattern. I'll write a
function which writes each individual parameter on its own line:
```d
void writelines(T...)(T values)
{
import std.stdio;
static foreach(v; values)
writeln(v);
}
```
Every combination of every type creates a new template
instantiation. We only save on instantiations when 2 calls happen
to match all their types.
But this function that is instantiated is all just calls to
`writeln`! It's not an interesting function, nor is it really
worthy of applying a combinatoric solution. We aren't getting any
special optimization by having the entire list in view at once.
Let's take an example call:
```d
writelines(1, 2, 3, 4);
```
Note how we had to *type out* each parameter individually, in the
same order they would be processed in the loop. Well, we can
write this ourselves!
```d
writeln(1); writeln(2); writeln(3); writeln(4);
```
This accomplishes the same thing, but instead of a template
instantiation per group of writes, we only get one instantiation
of `writeln` for integers. We effectively have removed the
combinatorics.
Now, this is quite the ugly solution! We have to repeat the call
for each one.
But notice how we have written the same exact list! But instead
of `", "`, our separator is `"); writeln("`.
What if we could reduce that separator? Maybe we can use an
`opCall`?
```d
struct WriteLines
{
ref opCall(T)(T val) {
import std.stdio;
writeln(val);
return this;
}
}
enum writelines2 = WriteLines.init;
```
Now, how does this look?
```d
writelines2(1)(2)(3)(4);
```
Our separator has changed into `")("`. A little nicer, but still
looks weird.
But more importantly, we have *one* instantiation of the `opCall`
for *all integers*. I can write any number of integers, or any
combination of integers and strings, or anything else, and we
only get one instantiation per type. The combinatorics are gone,
and yet I'm *mostly* writing the same thing.
How about we try an operator? Wait, isn't there another language
that does this?
```d
struct WriteLinesCpp
{
ref opBinary(string s: "<<", T)(T val) {
import std.stdio;
writeln(val);
return this;
}
}
enum coutlines = WriteLinesCpp.init;
```
And the usage is as you would expect:
```d
coutlines << 1 << 2 << 3 << 4;
```
Again, the benefit here is less combinatorics -- one
instantiation per type -- and less junk functions which are
unrolling the unrolled loops that we typed in the first place.
But... I still want to write `writelines(1, 2, 3, 4)`. The
ergonomics there are nice! Is there some way we can capture this
same reduction in complexity while still keeping the nice syntax?
I'll leave it up to the experts here to think about. I can
probably think of ways, but I'm sure they would not stand up to
scrutiny.
-Steve
More information about the Digitalmars-d
mailing list