The question mark goes on the left of the pointer, not the right. The list itself is not optional, but the elements (the strings) are. If we break it down:
[*:null] // many-item 'null'-terminated pointer to
? // optional of
[*:0]const // many-item '0'-terminated const pointer to
u8 // unsigned 8-bit integer
Perhaps the best way to explain complicated pointer types is to use Zig itself:
comptime {
explainType([*:null]?[*:0]const u8);
}
fn explainType(comptime T: type) void {
comptime {
@compileLog("Explaining " ++ @typeName(T));
var maybe_current: ?type = T;
while (maybe_current) |current| {
const info = @typeInfo(current);
@compileLog(info);
maybe_current = switch (info) {
inline .Array,
.Vector,
.Pointer,
.Optional,
=> |parent_info| parent_info.child,
else => null,
};
}
}
}
Pasting this into any source file and compiling will fail the compilation but print the following useful information to the terminal:
@as(*const [33:0]u8, "Explaining [*:null]?[*:0]const u8")
@as(builtin.Type, .{ .Pointer = .{.size = .Many, .is_const = false, .is_volatile = false, .alignment = 8, .address_space = .generic, .child = ?[*:0]const u8, .is_allowzero = false, .sentinel = null} })
@as(builtin.Type, .{ .Optional = .{.child = [*:0]const u8} })
@as(builtin.Type, .{ .Pointer = .{.size = .Many, .is_const = true, .is_volatile = false, .alignment = 1, .address_space = .generic, .child = u8, .is_allowzero = false, .sentinel = 0} })
@as(builtin.Type, .{ .Int = .{.signedness = .unsigned, .bits = 8} })