C/C++ macro challenge #1: BOOST_PP_COUNTER

This is the first of a hopefully long series of threads where we’ll take some C/C++ macros used by real application and try to find the equivalent (or perhaps better) solution using Zig. The aim is not to replicate the exact behavior of the C code. Generally that’s not possible. The preprocessor just works differently. The aim is to solve the actual problem at hand. I suspect that we’ll often need to take very different approaches.

The present example is case in point. BOOST_PP_COUNTER is a macro that yield a incrementing number. Simple enough and yet impossible to implementing using comptime programming due to the difference in execution order (among other things). What we can implement though is a comptime counter, something that yields a new number everytime it’s invoked at comptime. So that’s what’s we’re aiming for here.

For you convenience I’ve posted the documentation for BOOST_PP_COUNTER below:


The BOOST_PP_COUNTER macro expands to the current counter value.

Usage

BOOST_PP_COUNTER

Remarks

This macro expands to an integer literal. Its initial value is 0. Between usages of BOOST_PP_UPDATE_COUNTER, the value of BOOST_PP_COUNTER is constant.

See Also

Requirements

Header: <boost/preprocessor/slot/counter.hpp>

Sample Code

#include <boost/preprocessor/slot/counter.hpp>

constexpr int A  = BOOST_PP_COUNTER; // 0

#include BOOST_PP_UPDATE_COUNTER()

constexpr int B = BOOST_PP_COUNTER; // 1

#include BOOST_PP_UPDATE_COUNTER()

constexpr int C = BOOST_PP_COUNTER; // 2

#include BOOST_PP_UPDATE_COUNTER()

constexpr int D = BOOST_PP_COUNTER; // 3

EDIT: Modified sample code to make usage clearer

3 Likes

I can think of two potential approaches to this. Both use the uniqueness of anonymous opaque {} types, which has also been (ab)used for other interesting purposes in the past.

The first one is slightly less hacky, but has the major disadvantages that 1. the returned value is not comptime-known and 2. it wastes one byte of space for each counter value in the final binary:

const std = @import("std");

pub fn counter(comptime T: type) usize {
    if (@typeInfo(T) != .Opaque or @typeInfo(T).Opaque.decls.len != 0) {
        @compileError("pass opaque {} as the argument");
    }

    return @intFromPtr(&struct {
        var t: u8 = 0;
    }.t);
}

pub fn main() !void {
    std.debug.print("{}\n", .{counter(opaque {})});
    std.debug.print("{}\n", .{counter(opaque {})});
}

(as described in [Feature Request] @typeInfo of a `type` with a unique identifier · Issue #5459 · ziglang/zig · GitHub; the linked issue being solved would make this more natural)

The second approach is more reliant on implementation details, but it does work at comptime and should not waste any space:

const std = @import("std");

pub fn counter(comptime T: type) comptime_int {
    if (@typeInfo(T) != .Opaque or @typeInfo(T).Opaque.decls.len != 0) {
        @compileError("pass opaque {} as the argument");
    }
    const type_name = @typeName(T);
    const qualifier_index = std.mem.lastIndexOfScalar(u8, type_name, '_') orelse
        @compileError("pass opaque {} as the argument");
    var value: comptime_int = 0;
    for (type_name[qualifier_index + 1 ..]) |c| {
        value = 10 * value + (@as(comptime_int, c) - '0');
    }
    return value;
}

const a = counter(opaque {});
const b = counter(opaque {});

pub fn main() !void {
    std.debug.print("{}\n", .{a});
    std.debug.print("{}\n", .{b});
    std.debug.print("{}\n", .{counter(opaque {})});
    std.debug.print("{}\n", .{counter(opaque {})});
}
1 Like

Here’s the solution I came up with:

// counter.zig
const std = @import("std");

pub fn getCounter(comptime scope: anytype, comptime starting_value: comptime_int) type {
    _ = scope;
    return create: {
        comptime var current_value: comptime_int = starting_value;
        break :create struct {
            pub fn get(comptime inc: *const i8) comptime_int {
                current_value += inc.*;
                return current_value;
            }

            pub fn next() *const i8 {
                comptime var inc: i8 = 1;
                return &inc;
            }

            pub fn previous() *const i8 {
                comptime var inc: i8 = -1;
                return &inc;
            }

            pub fn current() *const i8 {
                comptime var inc: i8 = 0;
                return &inc;
            }
        };
    };
}

scope is used to indicate the scope of the counter. If you want to limit the counter to the current, pass @This() as shown below:

// main.zig
const std = @import("std");
const counter = @import("./counter.zig").getCounter(@This(), -1);
const next = counter.next;
const current = counter.current;

const A = counter.get(next());
const B = counter.get(next());
const C = counter.get(next());
const D = counter.get(next());
const E = counter.get(current());

fn printNumber(value: u32) void {
    std.debug.print("printNumber({any}) -> {d}\n", .{ value, counter.get(next()) });
}

fn printComptimeNumber(comptime value: u32) void {
    std.debug.print("printComptimeNumber({any}) -> {d}\n", .{ value, counter.get(next()) });
}

fn printAny(value: anytype) void {
    std.debug.print("printAny({any}) -> {d}\n", .{ value, counter.get(next()) });
}

pub fn main() void {
    std.debug.print("{d} {d} {d} {d} {d}\n", .{ A, B, C, D, E });

    printNumber(100);
    printNumber(100);
    printNumber(101);

    printComptimeNumber(200);
    printComptimeNumber(200);
    printComptimeNumber(201);

    printAny("Hello");
    printAny("World");
    printAny(1234);

    @import("./dmitri.zig").print();
    @import("./ivan.zig").print();
    @import("./alyosha.zig").print();
    const shared_counter = @import("./counter.zig").getCounter(.karamazov, 0);
    std.debug.print("Karamazov -> {d}\n", .{shared_counter.get(current())});
}

Dmitri, Ivan, and Alyosha all use the enum literal.karamzov as scope to indicate a shared counter:

// dmitri.zig
const std = @import("std");
const counter = @import("./counter.zig").getCounter(.karamazov, 0);
const next = counter.next;

pub fn print() void {
    std.debug.print("Dmitri -> {d}\n", .{counter.get(next())});
}
// ivan.zig
const std = @import("std");
const counter = @import("./counter.zig").getCounter(.karamazov, 0);
const next = counter.next;

pub fn print() void {
    std.debug.print("Ivan -> {d}\n", .{counter.get(next())});
}
// alyosha.zig
const std = @import("std");
const counter = @import("./counter.zig").getCounter(.karamazov, 0);
const next = counter.next;

pub fn print() void {
    std.debug.print("Alyosha -> {d}\n", .{counter.get(next())});
}

Output:

0 1 2 3 3
printNumber(100) -> 4
printNumber(100) -> 4
printNumber(101) -> 4
printComptimeNumber(200) -> 5
printComptimeNumber(200) -> 5
printComptimeNumber(201) -> 6
printAny([5:0]u8@20ef63) -> 7
printAny([5:0]u8@20ef69) -> 7
printAny(1234) -> 8
Dmitri -> 1
Ivan -> 2
Alyosha -> 3
Karamazov -> 0

EDIT: Made printAny() non-comptime (as actually intended). Add printing of counter value at the end to illustrate potential pitfall.

4 Likes
$ cat main.zig
const std = @import("std");

pub fn main() void {
    comptime var counter: struct {
        value: usize = 0,

        fn update(self: *@This()) void {
            self.value += 1;
        }
    } = .{};

    inline for (0..10) |_| {
        std.debug.print("value: {}\n", .{counter.value});
        comptime counter.update();
    }
}

$ zig run main.zig
value: 0
value: 1
value: 2
value: 3
value: 4
value: 5
value: 6
value: 7
value: 8
value: 9

A simple comptime variable would do, but you wanted a counter! I could also just use a capture for the for range instead, but that’s defeating the purpose of the exercise.

(I managed to crash the compiler while monkeying around with this :face_with_peeking_eye:)