Thread overview
Reducing variadic template combinatorics (C++ was onto something)
5 hours ago
monkyyy
3 hours ago
Daniel N
5 hours ago

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:

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:

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!

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?

struct WriteLines
{
    ref opCall(T)(T val) {
        import std.stdio;
        writeln(val);
        return this;
    }
}

enum writelines2 = WriteLines.init;

Now, how does this look?

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?

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:

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

5 hours ago

On Tuesday, 14 October 2025 at 04:30:49 UTC, Steven Schveighoffer wrote:

>

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.

[...]

I have fun ideas but youd need a benchmark; while I think I know how to estimate the big O of dmd, theres always the fun edge cases

3 hours ago

On Tuesday, 14 October 2025 at 04:30:49 UTC, Steven Schveighoffer wrote:

>

Now, how does this look?

writelines2(1)(2)(3)(4);

-Steve

It's probably not what you want...
[1,2,3,4].writeln;
... but I think the syntax is sufficiently nice.