Software Reliability C++ vs Zig

A tiny comparison of C++ and Zig in terms of building reliable software

I have spent a lot of my life writing C++ code and if there has been one thing that has bothered me about C++ it is just how fragile it is and generally unhelpful in tracking down bugs.

I don’t have time for an exhaustive check here, so here is just the simplest example I could come up with to make a comparison. We are reading a file which doesn’t exist. In C++ we got:

#include <iostream>
#include <fstream>
#include <string>

using namespace std;
int main (int argc, char const *argv[]) {
ifstream file("nonexistingfile.txt");

char buffer[1024];
file.read(buffer, sizeof(buffer));

cout << buffer << endl;

file.close();
return 0;
}

When I run this it gives me absolutely no output. Nothing tells me the file was not there or that anything went wrong.

Let us look at the equivalent program in Zig:

const std = @import("std");

usingnamespace std.fs;

pub fn main() !void {
const stdout = std.io.getStdOut().writer();

const file = try cwd().openFile(
"nonexistingfile.txt",
.{ .read = true },
);
defer file.close();

var buffer: [1024]u8 = undefined;
const size = try file.readAll(buffer[0..]);

try stdout.writeAll(buffer[0..size]);
}

If I run this I actually get a full stack backtrace:

error: FileNotFound
/usr/local/Cellar/zig/0.7.0/lib/zig/std/os.zig:1196:23: 0x10b3ba52e in std.os.openatZ (fileopen)
ENOENT => return error.FileNotFound,
^
/usr/local/Cellar/zig/0.7.0/lib/zig/std/fs.zig:754:13: 0x10b3b857e in std.fs.Dir.openFileZ (fileopen)
try os.openatZ(self.fd, sub_path, os_flags, 0);
^
/usr/local/Cellar/zig/0.7.0/lib/zig/std/fs.zig:687:9: 0x10b3b6c4b in std.fs.Dir.openFile (fileopen)
return self.openFileZ(&path_c, flags);
^
~/Development/Zig/fileopen.zig:8:18: 0x10b3b6810 in main (fileopen)
const file = try cwd().openFile(

I can actually easily jump in and look at the Zig standard library at where things went wrong, or just look at the message in the top which says file not found.

Now, some may argue that I only get this backtrace because I handled the error. Like I wrote try in front of cwd().openFile. Except Zig will not let not handle this potential error. If I remove the try I get:

fileopen.zig:15:24: error: type 'std.fs.file.OpenError!std.fs.file.File' does not support field access
const size = try file.readAll(buffer[0..]);

This requires a bit explanation I am trying to do read from an object which is of type OpenError!File rather than just File. Basically I got a union type. It it sort of an optional, rather than being a value or null, it is an actual value or an error code. You got to deliberately unwrap the value before you can use it.

What if I forget to get the number of bytes read, and thus specify wrong range in buffer to print out?

const size = try file.readAll(buffer[0..]);

Basically what happens if we remove the const size? Nope, Zig is not happy about that either:

fileopen.zig:15:5: error: expression value is ignored
try file.readAll(buffer[0..]);
^
writer.zig:25:31: note: referenced here
pub fn writeAll(self: Self, bytes: []const u8) Error!void {
^
writer.zig:25:56: note: referenced here
pub fn writeAll(self: Self, bytes: []const u8) Error!void {
^
fileopen.zig:17:15: note: referenced here
try stdout.writeAll(buffer[0..]);
^
fileopen.zig:5:21: note: referenced here
pub fn main() !void {

Now to be honest I am not really a static typing fan. I prefer writing code in Julia, but dynamic languages at least tend to do quite rigid type checking at runtime and give you nice stack backtraces informing you what went wrong. Unless we speak JavaScript which seems to like to paper over every problem it encounters.

These stack backtraces in Zig actually impress me quite a lot since it is a pretty low level language and not a script language and yet we get these very informative stack backtraces. In fact I don’t get that nice backtrace in Julia or Python when I tried for this particular case.

Ok before finishing off let us do one more little comparison, as I just remembered one little thing I think is quite an embarrassment to C++:

int main (int argc, char const *argv[])
{
int size = 10;
for(int i = 0; size; ++i) {
cout << "Hello world" << endl;
}

return 0;
}

I got this simple program where I forgot to write i < size and wrote just size instead where my condition was. Oops I got an infinitely running loop.

Here is the Zig version:

pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const size = 10;
var i: i32 = 0;
while (size) : (i += 1) {
try stdout.writeAll("Hello world\n");
}
}

Just as you would expect this does not compile, instead we get:

badloop.zig:7:12: error: expected type 'bool', found 'comptime_int'
while (size) : (i += 1) {
^
badloop.zig:3:21: note: referenced here
pub fn main() !void {
^

And honestly this is how it should work. Most modern languages today will insist on a boolean type in your control-flow statements. Sure in the defense of C++, it tried to be backwards compatible with C, and things have advanced since then.

But it is still an interesting example of how a more modern and more type safe language can help you build quality software faster. Keep in mind a lot of this stuff is not unique to Zig. You could get a lot of these kinds of improvements by picking Rust, D, Swift or Go over C/C++. Zig however has a lot of the same low level functionality C programmers like, such as manual memory allocation and deallocation, a C friendly ABI so you can easily reuse all your existing C libraries.

Written by

Geek dad, living in Oslo, Norway with passion for UX, Julia programming, science, teaching, reading and writing.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store