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

@chung-leong, question about this code’s behavior - if I invoke the boost counter in different files, does it keep one single counter running between all files, or does each file get it’s own independent counter that starts at zero?

–

@AndrewCodeDev According to the doc, the numbers are "unique within a translation unit". I take that to mean there's a single counter between all files.

–

3 Answers

3

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

Interesting, I did not realize that type (T != U) when T and U are sequentially declared opaques. I suppose it makes sense, they are apparently unique. If T and U are u32 for example, everything returns true and finishes up. Well this is a fascinating approach and some grade A hacking - so far, this has my vote. Like you said, this is very dependent on future type taxonomy, but still very interesting none the less.

–

Very interesting approach. I think we can make it less reliant on implementation details by converting the typename to a big comptime_int, then pass that to a regular comptime id allocator. As long as the names are guaranteed to be unique, we would get unique comptime_ints. We wouldn't need to care about what's in the string at all. We would also be able to start the counter at any value we want and increment (or decrement) it at any interval.

–

It seems that struct{} has the same type uniqueness as opaque{} and it does not waste a byte.

–

@slonik-az just to clarify, the wasted byte in the first approach is not due to the opaque{} (that's just to ensure that the counter function is instantiated again and doesn't use any space), but because of the var t whose address is taken to get the unique value. Basically the first approach boils down to "non-zero-sized global variables must have unique addresses", and uses a single byte per global to achieve that.

–

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

It's also important to note that comptime var does not work at file-level scope. The BOOST_PP_COUNTER works global scope and within function calls because it works with the preprocessor. This answer only covers one segment of the required functionality.

–