The way you might read this type is “optional constant pointer to mutable slice”. That’s an uncommon thing to want. As @squeek502 points out, you have a pointer to a pointer here.
This is a constant pointer to a zero-terminated 8-u8 array. The problem you’re running into is that pointers don’t peer coerce like this.
It would help with this kind of problem if you posted the definition of the struct with the code. My guess is that MyStruct looks like this:
In which case the signature of MyStruct.init should be pub fn init(str: ?[]const u8) as well.
However this is less idiomatic Zig than it could be. There’s two ways to represent an empty slice: there’s ?[]const u8, where the null case has a null pointer, and &.{}, which is a []const u8 where the .len field is zero.
You probably want a zero-length slice to represent emptiness, which would look like this:
pub fn init(str: []const u8) MyStruct {
// You can use anonymous construction
// because the result location is known
return .{ .str = str};
}
const struct_contents = MyStruct.init("contents");
const struct_empty = MyStruct.init("");
You’ll find that this is a better representation for most algorithms, and you can check for the empty case when you need to with if (my_struct.len == 0).
Thanks for such a detailed answer and the warm welcome!
As you can tell, I’m new to the community and zig in general, I have some thoughts and questions about this, would love to hear what you think of this
The way you might read this type is “optional constant pointer to mutable slice”. That’s an uncommon thing to want
Yupp, we can read it this way and now that I think about it, it’s not what I want. Instead what I want is a constant pointer to a zero terminated u8 array (or string!) So in that essence I already have what I want.
The end goal is to have the following pointer - optional constant pointer to constant slice. On further thoughts I can have a constant pointer which points to an optional bytes array or a null pointer, but I doubt thats the right way of doing things
*const [N:0]u8 // where N can be arbitrary, just that is has to be 0 terminated
The way I’m thinking is that it gives us the flexibility to pass any string to the function without actually duplicating the underlying data
Yeah I can check whether or not the string is passed by doing a len check, I thought of using ? instead to make it more apparent to the reader that this block of bytes could be null as well.
Is that a common enough problem, or is it me “wanting” the wrong thing and going against the zig way of doing things
Arrays are value types in Zig, and their length is always known at compile-time. A slice is a pointer + a length, and the length is runtime-known.
As an example, consider this code:
const std = @import("std");
test "value types as slice" {
const arr: [4]u8 = .{0} ** 4;
// Pointer to array coerces to a slice
try foo(&arr);
const num: u32 = 0;
try foo(std.mem.asBytes(&num));
}
fn foo(slice: []const u8) !void {
if (slice.len != 4) return error.UnexpectedLength;
}
The above test will pass, and from the perspective of foo, it doesn’t have to care at all about whether slice points to an array or the bytes of a u32 (or anything else). It just sees a pointer to len bytes of memory.
If you want a type that can have a runtime-known length and is NUL-terminated, then you can use a sentinel-terminated slice type, e.g. [:0]const u8. However, if you don’t have a specific reason in mind for the sentinel terminator (most commonly, you will be passing it to a C function that needs the terminator), using a sentinel-terminated type isn’t necessary. As mentioned, slices encode the length of the data they point to, so there’s normally no need for the sentinel terminator if you’re just working with slices in pure Zig code.
This also depends on whether you’re doing interop with C. In Zig we use empty slices almost exclusively, and don’t normally use a sentinel 0 for “strings”. In quotes because Zig does have string literals, but not an explicit string type.
The main reason to use empty slices is that Zig is null-safe. A T can never be null, only a ?T can be null, and you must check before you can use it. So a ?[]const T has two ways it can be “empty”, because even if there’s a slice there, the length can also be zero. This is common problem in Go, where everything can be nil.
In Zig we can opt out of the problem and just use []T or []const T, so you should think of ?[]const T as “this may or may not be a slice, which may or may not be empty”.
Even for C interop, it’s very common for an empty string to be represented by a valid char * which points to a zero byte, rather than a NULL char * which is illegal to reference. C also has to ‘understand’ both kinds of empty, since it isn’t a null-safe language either.
Here’s how you can get that:
const empty_str = "";
The type of empty_str is: *const [0:0]u8. That’s a constant pointer to an array of u8, with zero ‘payload’ bytes, and one sentinel byte, which is also 0.
Within Zig, you can type coerce this to a [:0]const u8, to keep the knowledge of the sentinel in the type system, and you can also coerce it to []const u8 if your code doesn’t need to know about the sentinel. String literals are always stored in .rodata, constant memory, so they can’t be mutated and they have static duration. You get the null-terminator automatically, for API compatibility with C (offer only applies to string literals!).
Type coercion means it will convert automatically if you pass it to a parameter, or assign it to a variable, which wants to be that type. If you have an array, not a pointer to an array, then you need to take the address to coerce to a slice.
So: if your function requires the sentinel 0, the signature should be [:0]const u8. But if it doesn’t eventually pass the .ptr field to a C function, it probably doesn’t need the sentinel, and []const u8 is the better type signature. Zig code usually has to go to extra effort to put a zero-terminal on slices, so don’t impose that restriction if you don’t need it.
This all sounds more complex than it is, but it does take some practice to get your bearings. I suggest using an editor in conjunction with zls and turning on type hints, it does a good job of showing what type something actually is. If you’re not sure you can assign something to a variable with a type annotation, and if it’s wrong the compiler will tell you why.