Metaprogramming in Zig & C++

I think you could replicate alot Zig code (with enough mental gynastics, and with a few exceptions) in C++. Even so, I think there's alot to appreciate in the Zig language.

I like Zigs simplicity. C++ has many flavours of metaprogramming (templates, reflection, constexpr) and Zig has just one. Zig treats types to first-class values at compile time, making Zig metaprogramming easy to pick up and straightforward to use.

Here's an example of what I mean. Lets say you wanted to generate the header of a CSV file based on a struct, where eg. struct Point { float x, float y} becomes "x, y" entirely at compile time. Here's code in C++:

template<typename T> 
consteval std::string generate_columns() {
    std::string columns;
    bool first = true;
    constexpr auto members = std::meta::nonstatic_data_members_of(^^T);
    template for (constexpr std::meta::info member : members) {
        if (!first)
            columns += ", ";
        first = false;
        columns += std::meta::identifier_of(member);
    }
    return columns;
}

Complaining about conceptual baggage in C++ is boring, but let me labour the point a little. To write the above code we have to pretty strongly understand: (1) templates, (2) consteval and (3) reflection. Even more so, you have to identify the negative space of language features you shouldn't use. C-style macros? Runtime type information (RTTI)? What about vibe coding consteval std::string get_column_names() on every class, and a C++ 20 concept definition?

Anyway, here's what identical code looks like in Zig:

fn generateColumns(comptime T: type) []const u8 {
    var columns: []const u8 = "";
    const fields = @typeInfo(T).@"struct".fields;
    for (fields, 0..) |field, i| {
        if (i > 0) {
            columns = columns ++ ", ";
        }
        columns = columns ++ field.name;
    }
    return columns;
}

You could dismiss some of Zig's improvements on C++ as "clearer syntax" or "better user experience", but most of this falls out of design decisions deeper within the language:

Again, neither of these are showstoppers for C++. You can carry on using templates, reflection syntax, and mark everything with constexpr. When you inevitably run into an edge case, you'll dig up a tutorial on the "constexpr two-step", hopefully fix it or just give up. The language is undeniably harder, and more of its capabilities are out of reach.

The example that made me write this post

I love C++'s boost::accumulators API, but I could not reimplement it. Here's what it looks like:

// a new type for just the mean and variance
accumulator_set<double, stats<tag::mean, tag::variance(lazy)>> acc;

// push in some data ...
acc(1.2);
acc(2.3);

// Display the results ...
std::println("Mean: {}, Variance: {}", mean(acc), variance(acc));

The whole point of this library is that hand-written statistical accumulators can do less arithmetic than the sum of their individual components. At compile time, Boost somehow determines that mean and variance both depend on the sum, and the count of datapoint, and it doesn't calculate them twice:

struct mean {
    double sum_of_x;
    size_t count_of_x;
};

struct variance {
    double sum_of_x;
    double sum_of_x_squared;
    size_t count_of_x;
};

// avoids redundant calculations

struct hand_written_acc {
    double sum_of_x;
    double sum_of_x_squared;
    size_t count_of_x;
};

// this is what Boost produces!

How would you write the Boost library in C++? If I were to guess, I would say that you need to create a distinction between raw quantities that you store and statistics you output.

// quantities: things you store
struct Sum { double value; };
struct SumOfSquares { double value; };
struct Count { size_t value; };
// statistics: things you output
struct Mean;
struct Count;
struct Variance;

You want to have some way of storing a variable number of quantities in a single statistical object. C++ has variadic templates, which can feed into a std::tuple:

// we instantiate eg. Accumulator<Sum, Count>
template <class... Qs>
class Accumulator {
    std::tuple<Qs...> tuple;

    operator()(double v) {
        // insert `v` to every quantity in `tuple`
        std::apply(
            [v](auto&... q) { (q.update(v), ...); }, tuple
        );
    }

    template<>
    double get<Mean>() const {
        return std::get<Sum>(tuple).value / std::get<Count>(tuple).value;
    }
}

Even if this doesn't match Boost's API, I think this is super satisfying. The user specifies exactly what intermediary results they want to calculate and C++ variadic templates let you compose exactly the object that you need.

Accumulator<Sum, Count> acc; 
auto m = acc.get<Mean>();

Unfortunately, if a user wants the mean and the variance, they have to determine that the variance needs an extra SumOfSquares term, and update the accumulator object:

Accumulator<Sum, Count> acc; 
auto m = acc.get<Mean>();
auto v = acc.get<Variance>(); // COMPILE ERROR

Boost does the resolving from <Mean, Variance> to <Sum, SumOfSquares, Count> and I have no idea how to implement this. C++ metaprogramming has an astonishingly sharp skill curve, and perhaps I have a skill issue.

Rewrite in Zig?

Zig types are just expressions at compile time. The standard way to write generics in Zig is to write a function that takes a type, then construct and return a new type.

fn DoubleOrNothing(comptime win: bool) type {
    return struct {
        item: f64 if (win) else void;
    }
}

var x: DoubleOrNothing(true) = .{.item = 0.0};

This is isomorphic to templates in C++, and you can achieve the same thing as above with partial template specialization or std::conditional_t in C++. But Zig is just cool. Types as expressions and compile time duck typing makes Zig feel like writing Python.

As a bonus, you are able to write something like Boost's compile time dependency resolution in Zig, which you'd really struggle to write in C++. Dependency resolution is conceptually just a mapping from one list of types to another:

fn resolve(comptime types: []const type) []const type {

A minimal and faithful re-implementation of boost::accumulator is just less than 100 lines of Zig. The public API does the aformentioned compile-time dependency resolution:

pub fn main() !void {
    var acc: Accumulator(.{ Mean, Variance }) = .{};
    acc.update(3.0);
    acc.update(3.0);
    acc.update(4.0);
    std.debug.print("mean={d}\n", .{acc.result(Mean)});
    std.debug.print("variance={d}\n", .{acc.result(Variance)});
}

The core implementation of Accumulator is actually quite elegant. We take in an anytype, coerce it to a slice of types and perform dependency resolution. When we update, we update each individual field in state. Getting a result is classic dependency injection, where the input type (eg. Mean) provides a .calculate() method to calculate the statistic.

fn Accumulator(comptime stats: anytype) type {
    // eg. resolve([Mean]) returns [Sum, Count]
    const quantities = comptime resolve(&stats);

    return struct {
        // store @Tuple([Sum, Count])
        state: @Tuple(quantities), // default value todo

        fn update(self: *@This(), item: f32) void {
            // update sum and count
            inline for (&self.state) |*field| field.update(item);
        }
        fn result(self: *@This(), S: type) f32 {
            // calculate the statistic using state
            return S.calculate(self);
        }
        fn get(self: *@This(), Q: type) f32 {
            // get any stored quantity in state
            inline for (self.state) |field| {
                if (@TypeOf(field) == Q) return field.val;
            }
        }
    };
}

Here's the rough implementation of the statistics and quantities respectively:

const Sum = struct {
    val: f32 = 0,
    fn update(self: *@This(), item: f32) void {
        self.val += item;
    }
};

const Count = struct {
    val: f32 = 0,
    fn update(self: *@This(), _: f32) void {
        self.val += 1.0;
    }
};
const Mean = struct {
    const deps = [_]type{ Count, Sum };
    fn calculate(state: anytype) f32 {
        return state.get(Sum) / state.get(Count);
    }
};

const Variance = struct {
    const deps = [_]type{ Count, Sum, SumOfSquares };
    fn calculate(state: anytype) f32 {
        const n = state.get(Count);
        const s = state.get(Sum);
        const ss = state.get(SumOfSquares);
        return ss / n - (s / n) * (s / n);
    }
};

The full implementation has a teeny weeny bit more boilerplate, but it's remarkably coherent.

Closing Notes

To be honest, I still like bits of C++. There's convenience in RAII, global allocators and in having pretty much every language feature of the last half century at your fingertips. C++ is de-facto standard and a powerful language once you've internalized its quirks.

Zig, by contrast, strikes tradeoff between complexity and clarity that is rare for a systems programming language. The syntax is refreshing, and comptime alongside a sprinkle of structural typing makes metaprogramming feel like writing Python. The community is small, but fanatically ambitious. They're currently working on writing their own Zig compiler backend to replace LLVM as well as porting libc to Zig. I'm excited for the language.