Sentinel-terminated Arrays
A sentinel-terminated array is an array with one extra element to mark the end. They’re great for compatilibity with C APIs, where zero-terminated arrays are a common convention, but we can also just use them in Zig:
const std = @import("std");
const print = std.debug.print;
pub fn main() void {
// [3:0] means length 3 and 0 as sentinel
const array = [3:0]u64{ 100, 200, 300 };
print("array length: {}\n", .{array.len});
print("size in bytes: {}\n", .{
@sizeOf(@TypeOf(array)),
});
print("elements:", .{});
print(" {}", .{array[0]});
print(" {}", .{array[1]});
print(" {}", .{array[2]});
print(" {}", .{array[3]});
print("\n", .{});
}$ zig run sentinel.zig
array length: 3
size in bytes: 32
elements: 100 200 300 0
Sentinel-terminated pointers
A sentinel-terminated pointer points into a sentinel-terminated array:
const std = @import("std");
const print = std.debug.print;
fn printNumbers(ptr: [*:0]const u64) void {
var i: usize = 0;
while (ptr[i] != 0) : (i += 1)
print("{}\n", .{ptr[i]});
}
pub fn main() void {
const array = [3:0]u64{ 100, 200, 300 };
printNumbers(&array);
}$ zig run sentinel-2.zig
100
200
300
Sentinel-terminated slices
A sentinel-terminated slice is a slice that also guarantees a sentinel value after the last element:
const std = @import("std");
const print = std.debug.print;
pub fn main() void {
const array = [3:0]u64{ 100, 200, 300 };
const slice: [:0]const u64 = &array;
print("used as pointer:\n", .{});
const ptr: [*:0]const u64 = slice.ptr;
for (0..4) |i|
print(" {}\n", .{ptr[i]});
print("used as slice:\n", .{});
const regular_slice: []const u64 = slice;
for (regular_slice) |n|
print(" {}\n", .{n});
}$ zig run sentinel-3.zig
used as pointer:
100
200
300
0
used as slice:
100
200
300
Allocating a sentinel-terminated array
We can request a sentinel-terminated slice from an allocator:
const std = @import("std");
const print = std.debug.print;
pub fn main(init: std.process.Init) !void {
const allocator = init.arena.allocator();
const slice = try allocator.allocSentinel(u64, 3, 0);
slice[0] = 100;
slice[1] = 200;
slice[2] = 300;
print("used as pointer:\n", .{});
const ptr: [*:0]const u64 = slice.ptr;
for (0..4) |i|
print(" {}\n", .{ptr[i]});
print("used as slice:\n", .{});
const regular_slice: []const u64 = slice;
for (regular_slice) |n|
print(" {}\n", .{n});
}$ zig run sentinel-4.zig
used as pointer:
100
200
300
0
used as slice:
100
200
300
Other sentinel values
The sentinel doesn’t have to be zero:
const std = @import("std");
const print = std.debug.print;
pub fn main() void {
// sentinel is -1
const numbers = [_:-1]i64{ 100, 200, 300 };
print("numbers:", .{});
print(" {}", .{numbers[0]});
print(" {}", .{numbers[1]});
print(" {}", .{numbers[2]});
print(" {}", .{numbers[3]});
print("\n", .{});
// sentinel is a null pointer
const a: u64 = 1;
const b: u64 = 2;
const pointers = [_:null]?*const u64{ &a, &b };
print("pointers:", .{});
print(" {?}", .{pointers[0]});
print(" {?}", .{pointers[1]});
print(" {?}", .{pointers[2]});
print("\n", .{});
// sentinel is an enum value
const Dir = enum { up, down, left, right, end };
const directions = [_:.end]Dir{ .left, .right };
print("enum values:", .{});
print(" {}", .{directions[0]});
print(" {}", .{directions[1]});
print(" {}", .{directions[2]});
print("\n", .{});
}$ zig run sentinel-5.zig
numbers: 100 200 300 -1
pointers: u64@1270948 u64@1270958 null
enum values: .left .right .end
Next example: Labeled Switch.