Are sentinels only intended for null terminators?

I wasn’t able to find any real world examples in Zig’s source code of how to properly terminate an array outside of the common null-terminator.

More of a curiosity than a problem that needs solved, but looking at the recent How to initialize an “empty” sentinel terminated array?, just having the ability to specify your own sentinel is generating some curiosity.

Is there a good reason why we can specify our own sentinel instead of being forced into zero-terminated that are used in C strings? Zig does appear to allow it.

var space = [_:' ']u8{ '0', '1', '2', '3', '4', '5' };
4 Likes

I haven’t used sentinels other than zero so I am not exactly sure.

Maybe you could use more than one sentinel to nest sentinel terminated arrays (would require some careful handling of those arrays) but should work, for example you could have one big array that uses a newline as sentinel and within that you could have multiple smaller sentinel terminated arrays that end with comma, that would basically be csv with trailing comma for the last value.
That is just a silly idea I came up with on the spot, I think I prefer normal slices.

There also might be cases where you write some kind of algorithm which may naturally end up in a terminating state if it is given a certain value that isn’t zero,
in that case it may make sense to use that value as a sentinel, so that the algorithm can just consume the input and just stop when it reaches the sentinel.

2 Likes

Sentinel-terminated anything is mostly useful for interoperating with C APIs, since if you’re dealing with Zig APIs, then regular ol’ slices are better in most situations.

So the usefulness of non-0 sentinels would be dependent on the C APIs you are interacting with. For example, the Lua C API has some functions that take an array of luaL_Reg structs where:

Any array of luaL_Reg must end with a sentinel entry in which both name and func are NULL.

In C, that looks like:

static const luaL_Reg luv_async_methods[] = {
  {"send", luv_async_send},
  {NULL, NULL}
};

In Zig, that could be expressed as something like (untested, might have the syntax here slightly wrong):

const luaL_RegTerminator = c.luaL_Reg{ .name = null, .func = null };
const luv_async_methods = [_:luaL_RegTerminator]c.luaL_Reg{
    .{ .name = "send", .func = luv_async_send },
};
9 Likes

Other common sentinels are also negative integers such as -1 and highest value of the stream aka std.math.maxInt(T)

5 Likes

Sounds like its there “just in case” you encounter one of those rare scenarios where a C API has a unique way of terminating an array. It also sounds like sentinels are best reserved for interfacing C APIs. Slices being more native and useful within Zig.

I’m open to hearing more examples or unique situations or being better informed how others use sentinels in real applications, but this satisfies my curiosity. Thank you everyone for the examples!

1 Like

A NULL terminated array of pointers is fairly common too. I just came across another example, also with Lua:

/// Zig wrapper for Luau lua_CompileOptions that uses the same defaults as Luau if
/// no compile options is specified.
pub const CompileOptions = struct {
    optimization_level: i32 = 1,
    debug_level: i32 = 1,
    coverage_level: i32 = 0,
    /// global builtin to construct vectors; disabled by default (<vector_lib>.<vector_ctor>)
    vector_lib: ?[*:0]const u8 = null,
    vector_ctor: ?[*:0]const u8 = null,
    /// vector type name for type tables; disabled by default
    vector_type: ?[*:0]const u8 = null,
    /// null-terminated array of globals that are mutable; disables the import optimization for fields accessed through these
    mutable_globals: ?[*:null]const ?[*:0]const u8 = null,
};

const options = ziglua.CompileOptions{
    .mutable_globals = &[_:null]?[*:0]const u8{ "foo", "bar" },
};
3 Likes