Optionals

One area that Zig provides safety without compromising efficiency or readability is with the optional type.

The question mark symbolizes the optional type. You can convert a type to an optional type by putting a question mark in front of it, like this:

  1. // normal integer
  2. const normal_int: i32 = 1234;
  3. // optional integer
  4. const optional_int: ?i32 = 5678;

Now the variable optional_int could be an i32, or null.

Instead of integers, let’s talk about pointers. Null references are the source of many runtime exceptions, and even stand accused of being the worst mistake of computer science.

Zig does not have them.

Instead, you can use an optional pointer. This secretly compiles down to a normal pointer, since we know we can use 0 as the null value for the optional type. But the compiler can check your work and make sure you don’t assign null to something that can’t be null.

Typically the downside of not having null is that it makes the code more verbose to write. But, let’s compare some equivalent C code and Zig code.

Task: call malloc, if the result is null, return null.

C code

  1. // malloc prototype included for reference
  2. void *malloc(size_t size);
  3. struct Foo *do_a_thing(void) {
  4. char *ptr = malloc(1234);
  5. if (!ptr) return NULL;
  6. // ...
  7. }

Zig code

  1. // malloc prototype included for reference
  2. extern fn malloc(size: size_t) ?*u8;
  3. fn doAThing() ?*Foo {
  4. const ptr = malloc(1234) orelse return null;
  5. // ...
  6. }

Here, Zig is at least as convenient, if not more, than C. And, the type of “ptr” is *u8 not ?*u8. The orelse keyword unwrapped the optional type and therefore ptr is guaranteed to be non-null everywhere it is used in the function.

The other form of checking against NULL you might see looks like this:

  1. void do_a_thing(struct Foo *foo) {
  2. // do some stuff
  3. if (foo) {
  4. do_something_with_foo(foo);
  5. }
  6. // do some stuff
  7. }

In Zig you can accomplish the same thing:

  1. fn doAThing(optional_foo: ?*Foo) void {
  2. // do some stuff
  3. if (optional_foo) |foo| {
  4. doSomethingWithFoo(foo);
  5. }
  6. // do some stuff
  7. }

Once again, the notable thing here is that inside the if block, foo is no longer an optional pointer, it is a pointer, which cannot be null.

One benefit to this is that functions which take pointers as arguments can be annotated with the “nonnull” attribute - __attribute__((nonnull)) in GCC. The optimizer can sometimes make better decisions knowing that pointer arguments cannot be null.

Optional Type

An optional is created by putting ? in front of a type. You can use compile-time reflection to access the child type of an optional:

test.zig

  1. const expect = @import("std").testing.expect;
  2. test "optional type" {
  3. // Declare an optional and coerce from null:
  4. var foo: ?i32 = null;
  5. // Coerce from child type of an optional
  6. foo = 1234;
  7. // Use compile-time reflection to access the child type of the optional:
  8. comptime expect(@typeInfo(@TypeOf(foo)).Optional.child == i32);
  9. }
  1. $ zig test test.zig
  2. 1/1 test "optional type"... OK
  3. All 1 tests passed.

null

Just like undefined, null has its own type, and the only way to use it is to cast it to a different type:

  1. const optional_value: ?i32 = null;

Optional Pointers

An optional pointer is guaranteed to be the same size as a pointer. The null of the optional is guaranteed to be address 0.

test.zig

  1. const expect = @import("std").testing.expect;
  2. test "optional pointers" {
  3. // Pointers cannot be null. If you want a null pointer, use the optional
  4. // prefix `?` to make the pointer type optional.
  5. var ptr: ?*i32 = null;
  6. var x: i32 = 1;
  7. ptr = &x;
  8. expect(ptr.?.* == 1);
  9. // Optional pointers are the same size as normal pointers, because pointer
  10. // value 0 is used as the null value.
  11. expect(@sizeOf(?*i32) == @sizeOf(*i32));
  12. }
  1. $ zig test test.zig
  2. 1/1 test "optional pointers"... OK
  3. All 1 tests passed.