Annoying things?

I don’t want to be the negative vibe guy, but I wonder if it’s actually positive to have a category on the forum for “Annoying things about Zig”? Like the saying about “real friends make you cry with the truth”, a place for people to report what they dislike (with respect and politely of course) could actually help in the evolution of the language. But maybe it can be just a thread and not a category per se. Anyway I would report that right up there in the top 10 annoying things has to be the

error: Expected tuple or struct argument, found...

when using any of the print functions and specifying bare arguments instead of putting them in a tuple! Arrrrgh!! Happens to me at least 10 times a day :smiley:


That took me a while to get used to, but I now prefer it since varags are now way easier to understand. The are also are no longer a part of the language, but simply an API decision.

My real gripe is the lack of operator overloading, since I mainly want to use Zig for 3D graphics. I’ve been contemplating a solution proposed from the an issue about it (don’t remember the number):

std.math.expr("cross(v1 * m1, sin(v3))", .{
  .v1 = v1,
  .v2 = v2,
  .m1 = m1,
  .cross = Vector3.cross

Still trying to determine how to implement it.


Yes, operator overloading can definitely make a language really fluid and expressive, but at the same time make it opaque or even “magical” in the sense of not knowing exactly what’s going on just by looking at the code. This goes in the opposite direction of Zig’s goals in terms of clarity and transparency. I guess that a “clear and transparent” operator overloading implementation would have to be one in which the overload function to operator mapping is static and 1 to 1. If I can be assured that using the + operator always (and can only) call the plus function, the opaqueness and magic goes away. But then we open up a whole new dimension of default implementations, allowed ops for types (as in Concepts in C++), etc.


Given that Zig is still pre-1.0 this is exactly the time to bring up as many complaints as possible, so that way we can make the future the best it can be.

Bad error messages (at this stage in development) are a bit low priority at the moment, but it is certainly good to remember where they are (or make a PR to fix them :smiley:) because as we get closer to Zig being done rounding out the pain points could be the difference between someone wanting to use Zig for every project or getting frustrated and giving up on it and telling their friends about their struggles too.

As this is something that is super contributor friendly, it would be a great place to get started if you haven’t contributed to Zig before :slight_smile: and feel free to ask any questions you run into while doing so.


Something annoying is that it’s still easy to shoot yourself in the foot:

const std = @import("std");

fn oopsie() *u32 {
    var x: u32 = 123;
    std.debug.print("&x: {*}\n", .{&x});
    return &x;

fn dosomething() void {
    var buffer: [100]u32 = undefined;

pub fn main() !void {
    var xptr = oopsie();
    std.debug.print("xptr: {}\n", .{xptr});
    std.debug.warn("xptr.*: {}\n", .{xptr.*});

Here I’m returning an address of a variable on the stack and nothing’s stopping me. But the value is silently being overwritten by later function calls:

$ zig run test.zig
&x: u32@7ffc814a1384
xptr: u32@7ffc814a1384
xptr.*: 0

This should definitely be a compile error, zig just doesn’t do it yet.


I personally would like to be able to do more things with enums. For example:

  • Define an enum and then iterate (with for?) over all its elements.
  • Associate a payload with enum members.
  • Define arrays indexed by enum members => with whatever restrictions make sense, so I would also like the compiler to warn me if I have enum foo { bar = 1, baz = 1000000 } and then try to create an array indexed by that: You dufus, that will create a sparse array with 1M elements -- go away.
1 Like

I would also like to be able to write a loop with the looping variable scoped to the loop itself, without having to declare it before the loop (which keeps it around after the loop) nor having to introduce an extra scope (which is just ugly and makes the code indentation be unnecessarily nested). I don’t really care much about the syntax, so while / for would be fine for me. Even adding loop would also work.

EDIT: I am aware of (and upvoted) the proposal to add

with(x: usize = 0) while (x < 10) {...}

although it is a tad verbose, but still, that works for me.

for (std.meta.fields(MyEnum)) |field| {
    // ... do a thing

Isn’t this roughly what Tagged Unions allow you to do?

You can convert enum values to comptime_ints with @enumToInt(). You’d still need to do the sizing of the underlying array manually, and if you don’t have holes in the numbering I would guess you can probably just create an array with length @typeInfo(MyEnumType).Enum.fields.len.

Letting the array be initialized to a very large number of elements is inadvisable. At which point should the compiler start giving out warnings? After 100 elements? After 500? After 1000?..

I was thinking more of associating a whole struct as the payload. Maybe even different types of payload for each enum value (a la Rust).

Yeah, but that is pretty ugly… Remember, these are just dreams of mine :slight_smile: – but for example, I would like to restrict these arrays to be indexed only by enums whose tags have consecutive values, and then be able to do stuff like this:

enum Direction { N, S, E, W };
var height: [Direction]usize;
height[N] = 1; height[S] = 44; height[E] = 23; height[W] = 20;
for (height) |h, d| { switch (d) { case S: ...} }

Sure, but you must admit this is quite ugly, compared to the example I just made up before, or compared to this:

for (Direction) |d| { ... } // looks better to me

Yes, this sort of works, though it’s a bit awkward to use IMHO:

$ cat >tun.zig <<-EOF
const std = @import("std");

const Thing = union(enum) {
    First: struct {
        hello: []const u8 = "hello",
    Second: struct {
        world: []const u8 = "world",

pub fn main() !void {
    const x = Thing{ .First = .{} };
    const y = Thing{ .Second = .{} };
    std.debug.print("{s} {s}\n", .{ x.First.hello, });

$ zig run tun.zig
hello world

Is this what you’re thinking of? zig/enums.zig at 3bf72f2b3add70ad0671c668baf251f2db93abbf · ziglang/zig · GitHub


Wow! Somehow I missed that. Yes, this is exactly what I was thinking of. Having direct syntax support for this in zig might be nicer, but this is certainly good enough for me. Thanks for the pointer!


Same here! :sweat_smile:I want to switch from C++ to zig in my scientific computation projects but is blocked by the lacking of operator overloading. Rust may be an alternative but I don’t want to spend time fighting with the borrow checker and lifetimes.

Also your solution is very interesting, and I think it can be easily implemented using a comptime expression parser.


Instead of this

if (optional) |v| {
    if (v == 42) foo() else bar();

We can do this

if (optional != null and optional.? == 42)  foo() else bar();

But wouldn’t this be nice?

if (optional) |v| and (v == 42) foo() else bar();

Not too sure about this syntax, but the idea is that this kind of “optional has value and value is” idiom is common, so maybe some syntactic sugar wouldn’t hurt?


I kind of like that syntax actually, too me it’s very clear what is going on.

Yours is nicer, but it’s not too far from this, which you can already do today:

if (optional) |v| if (v == 42) foo() else bar();

And it’s 1 character shorter! :laughing: I guess that with this being possible already, there’s no critical justification for modifying the language to permit the and version. Although I do find the and version more clearly conveys the dependency between the two conditions, but that’s my perception and maybe not everyone else’s.

1 Like

One nice thing would be to avoid having to explicitly assert that a nullable can be unwrapped if we’ve already checked that the_thing != null, similarly to how Dart does: