How to replace C++ operator overloading in Zig?

I’m translating a C++ code base (raytracing in one weekend) to Zig for learning and the original material heavily uses operator overload.

This C++ code looks simple, as I need to just subtract Vec3 structs from each other.

C++ code snippet:
    auto viewport_upper_left = camera_center - vec3(0, 0, focal_length) - viewport_u/2 - viewport_v/2;

In Zig, I had to make a function in my vec3 struct, so I can specifically subtract 4 vec3s to make this work.

First I tried to use all my functions in the vec3 struct and did this:

main.zig
    const viewport_upper_left = vec3.subs(camera_center, vec3.subs((vec3.init(0, 0, focal_length)), vec3.subs((vec3.scalarDiv(viewport_u, 2)), vec3.scalarDiv(viewport_v, 2))));

Needless to say, this made a bug in my code, where the upper left pixel wasn’t placed correctly.

So I broke up the subtraction to smaller pieces for readability, and made a function that has 4 Vec3 parameters that subtracts.

main.zig
    const viewport_a = vec3.init(0, 0, focal_length);
    const viewport_b = vec3.scalarDiv(viewport_u, 2);
    const viewport_c = vec3.scalarDiv(viewport_v, 2);
    const viewport_upper_left = vec3.subs4(camera_center, viewport_a, viewport_b, viewport_c);
vec3.zig
pub fn subs4(v: Vec3, w: Vec3, a: Vec3, b: Vec3) Vec3 {
    return .{
        .x = v.x - w.x - a.x - b.x,
        .y = v.y - w.y - a.y - b.y,
        .z = v.z - w.z - a.z - b.z,
    };
}

For this Zig implementation I challanged myself to not use any AI prompt, and try to minimize online searching to the zig documentation and a few zig learning sites. I feel like it’s a right time to ask this question, as I can see later this will be getting out of hand and the whole purpose is to learn to use the language correctly.

1 Like

Hello,
since Zig has no operator overloading its not simple 1 to 1 transformation.
But for this case, Zig has a build in Vector type which supports arithmetic operations and more. It also optimizes to SIMD if possible so its something you would want to do anyway (evetually).
If you would need help with something more specific, feel free to ask.

EDIT: you can also “alias” the vector type for convinience like this: const vec3 = @Vector(3, i32);

Robert :blush:

6 Likes

Zig’s builtin vectors should cover this case (and, correspondingly, 80% of the need for operator overloading). But using normal functions wouldn’t be too bad with some API design and judicious use of whitespace:

const viewport_upper_left = vsub(
    camera_center,
    vsum(.{
        .{0, 0, focal_length},
        vdiv(viewport_u, 2),
        vdiv(viewport_v, 2),   
    }),
)

Points of note:

  • As I expect to call these functions a bunch, I design them for being imported directly and used unadorned, vadd, vsub, vdiv, vmul.
  • I define vector as Vec3 = struct { f64, f64, f64 } so that I don’t need an init function.
  • sub4 doesn’t seem like a good function, it’s very specific. On the other hand, vsum that takes a tuple of vectors and sums them all up does sound like a re-usable helper, which can be built on top of vadd
  • Whitespace! Most of code’s readability comes from whitespace,
8 Likes

So I did what you said and imported the functions which helps readability now, but can’t decide which is better for me in the long run. I like the explicitness of vec3.add() too. (Maybe I just need more experience and will change my mind of this.)

However, the code still looks kinda funky. What I didn’t realize earlier, is that my subtraction orders were kinda backwards and that caused a “bug”.

So now the code behaves as intended and looks like this:

    const viewport_upper_left = vsub(vsub(vsub(
        camera_center,
        vec3.init(0, 0, focal_length)),
        vscalardiv(viewport_u, 2)),
        vscalardiv(viewport_v, 2)
    );

In your suggested code you have a vsum() but how does that look like? It would still look like my sub4 but with 3 parameters?

I had to turn off auto-format, which was a really good advice in order to use whitespaces better.
Still hard to read all the subtractions though.

I checked zmath for some SIMD math reference, and they are using @Vector as @Bobvan suggested. Maybe just for practice I should make my own math.zig file and try to do the same with @Vector but I really like how structs work in Zig.

Anyway I appriciate all the insight you guys give me when I ask these baby questions.

Something like this:

const std = @import("std");
const assert = std.debug.assert;

const Vec3 = struct { f64, f64, f64 };
fn vadd(a: Vec3, b: Vec3) Vec3 {
    return .{ a[0] + b[0], a[1] + b[1], a[2] + b[2] };
}

fn vsum(xs: anytype) Vec3 {
    const T = @typeInfo(@TypeOf(xs)).@"struct";
    comptime assert(T.is_tuple);
    if (T.fields.len == 0) return .{ 0, 0, 0 };
    var result: Vec3 = xs[0];
    inline for (1..T.fields.len) |i| {
        result = vadd(result, xs[i]);
    }
    return result;
}

pub fn main() void {
    const x: Vec3 = .{ 1, 0, 0 };
    const y = vsum(.{
        .{ 0, 1, 0 },
        x,
        .{ 0, 0, 1 },
    });
    std.debug.print("{}\n", .{y});
}

I had to turn off auto-format,

You don’t need to turn auto-format off, you need to add trailing commas. Try using zig fmt with this code to see the difference:

pub fn example() void {
    const y = vsum(.{
        .{ 0, 1, 0 },
        x,
        .{ 0, 0, 1 },
    });
    const z = vsum(.{
        .{ 0, 1, 0 },
        x,
        .{ 0, 0, 1 }
    });
}
4 Likes

FWIW this is my minimalist and cobbled-together matrix/vector module for the sokol-zig samples:

Code looks for instance like this:

I had switched from “mostly C++” to “mostly C” already around 2017, so the shock of not having operator overloading for vector/matrix math wasn’t quite as big.

FWIW I would really like Zig to have builtin types for at least vec[2,3,4][f,i,u], mat[2,3,4]x[2,3,4][f,i,u] - along with a handful of helper functions (basically: what GLSL, HLSL, WGSL and MSL have to offer).

4 Likes

@floooh any reason why you don’t define

const Vec2 = @Vector(2, f32);

?

I want to better understand when @Vector is appropriate. If I don’t necessary care about SIMD, and just want an algebraic vector, is the built-in appropriate? Or should I go for a struct? Don’t have great intuition here :frowning:

I feel like odin got it correct. It has built-in types for matrix and vectors which is like 90% of operator overloading.
Zig will probably never have them.

1 Like

There’s at least https://github.com/ziglang/zig/issues/7295 and https://github.com/ziglang/zig/issues/4960 still open

Hi!

I’m also doing the “Ray Tracing in One Week End” series to learn both RT and Zig. Still starting out in Zig, but I found the approach of having the operators as methods and chaining them together on multiple lines to be really nice and readable. Here’s my viewport_upper_left snippet:

const viewport_upper_left: math.Vec3 = self.center
    .sub(.vec3(0, 0, self.focal_length))
    .sub(viewport_u.div(2))
    .sub(viewport_v.div(2));

Also curious on whether @Vector is meant to be used in this case. I had tried it some point, but found that its performance in Debug to be worse than the simple struct approach. As I’d rather have faster builds than runs, I eventually scrapped it.

(Edit: off-topic, but the project makes also heavy use of interfaces and it was a bit annoying to union(enum) everything, but it’s okay in the end and I think the easiest approach in this case.)

3 Likes

IIRC one reason was that I wanted to have functions on the struct, e.g. being able to write const v0 = vec3.zero(), then there’s no @Matrix() builtin, so I had to use a struct for matrices anyway, and I also need a C compatible, defined memory layout since those structs are also used nested into extern structs as inputs to GPU shaders via sokol-gfx (maybe @Vector() can be part of an extern struct though, don’t remember if I tried that).

And then @Vector() doesn’t support the really good stuff from the Clang vector extension anyway (like swizzling), the really heavy processing stuff happens in GPU shaders anyway. If I’d need heavy SIMD-processing on the CPU side I would probably write specialized code anyway (maybe using @Vector(), but it would be nice if Zig had a proper set of SIMD-intrinsic like builtins - basically a complete implementation of the Clang extended vector extension: Clang Language Extensions — Clang 21.0.0git documentation

4 Likes

Awesome!

Yeah I can see your solution looks nice. I’ll think about it and try it later today, if I can get away from life things.
Good luck on Zig and RT!

1 Like

Thanks!

Sokol is something I want to later dwell deep into, and continue my game in. Currently I feel it’s too big of a step between raylib and sokol, but slowly I’m getting there.

1 Like