Hello there,
I have been using Zig for over 2 years now and did quite a lot in it.
Love the language and most of the decisions that have been made so far.
In my time using the language, I have build some custom data-structures that were not supported by standard library. Most of them were usecase specific, but one was comming back all the time . . .
SliceBuilder
Its quite simple, you have possibly runtime known size of a slice you want to create and combine your data in. You want to do it in a performant and debug safety checked way. Without data-structure, the code for it is often littered with @memcpy() math and if you are adding more then few items, its easy to make a mistake in it.
There are currently to my knowledge BoundedArray and FixedBufferStream, but both serve a bit different purpose and have runtime checks.
The MAIN QUESTION is, does it make sense for something like this to be in standard library? I find it really common pattern, but would love to get opinion from someone else. Also for code improvements!
This is my really simple implementation (unmanaged version without specified aligment), just to get the idea:
const std = @import("std");
pub fn SliceBuilder(comptime T: type) type {
return struct {
data: []T,
used: usize,
pub fn init(allocator: std.mem.Allocator, size: usize) !@This() {
return .{
.data = try allocator.alloc(T, size),
.used = 0,
};
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.data);
}
pub fn appendSingle(self: *@This(), value: T) void {
std.debug.assert(self.used < self.data.len);
self.data[self.used] = value;
self.used += 1;
}
pub fn appendSlice(self: *@This(), value: []const T) void {
std.debug.assert(self.used + value.len <= self.data.len);
@memcpy(self.data[self.used..][0..value.len], value);
self.used += value.len;
}
pub fn toSlice(self: @This()) []T {
std.debug.assert(self.used == self.data.len);
return self.data;
}
pub fn toSliceOwned(self: @This(), allocator: std.mem.Allocator) ![]T {
std.debug.assert(self.used == self.data.len);
const ret = try allocator.alloc(T, self.data.len);
@memcpy(ret, self.data);
return ret;
}
};
}
test "SliceBuilder init" {
const StringBuilder = SliceBuilder(u8);
const random_string_2 = "o World!";
var builder = try StringBuilder.init(std.testing.allocator, random_string_2.len + 4);
defer builder.deinit(std.testing.allocator);
builder.appendSlice("Hell");
builder.appendSlice(random_string_2);
try std.testing.expectEqualStrings("Hello World!", builder.toSlice());
try std.testing.expectEqual(12, builder.toSlice().len);
const my_slice = try builder.toSliceOwned(std.testing.allocator);
defer std.testing.allocator.free(my_slice);
try std.testing.expectEqualStrings("Hello World!", my_slice);
try std.testing.expectEqual(12, my_slice.len);
}
test "SliceBuilder init fail" {
const StringBuilder = SliceBuilder(u8);
try std.testing.expectError(
std.mem.Allocator.Error.OutOfMemory,
StringBuilder.init(std.testing.failing_allocator, 12),
);
}
test "SliceBuilder appendSingle()" {
const StringBuilder = SliceBuilder(u8);
var builder = try StringBuilder.init(std.testing.allocator, 5);
defer builder.deinit(std.testing.allocator);
builder.appendSingle('H');
builder.appendSingle('e');
builder.appendSingle('l');
builder.appendSingle('l');
builder.appendSingle('o');
try std.testing.expectEqualStrings("Hello", builder.toSlice());
try std.testing.expectEqual(5, builder.toSlice().len);
const my_slice = try builder.toSliceOwned(std.testing.allocator);
defer std.testing.allocator.free(my_slice);
try std.testing.expectEqualStrings("Hello", my_slice);
try std.testing.expectEqual(5, my_slice.len);
}
test "SliceBuilder appendSlice()" {
const StringBuilder = SliceBuilder(u8);
var builder = try StringBuilder.init(std.testing.allocator, 12);
defer builder.deinit(std.testing.allocator);
builder.appendSlice("Hel");
builder.appendSlice("lo");
builder.appendSlice(" ");
builder.appendSlice("Wo");
builder.appendSlice("rld");
builder.appendSlice("!");
try std.testing.expectEqualStrings("Hello World!", builder.toSlice());
try std.testing.expectEqual(12, builder.toSlice().len);
const my_slice = try builder.toSliceOwned(std.testing.allocator);
defer std.testing.allocator.free(my_slice);
try std.testing.expectEqualStrings("Hello World!", my_slice);
try std.testing.expectEqual(12, my_slice.len);
}
test "SliceBuilder append()" {
const StringBuilder = SliceBuilder(u8);
var builder = try StringBuilder.init(std.testing.allocator, 12);
defer builder.deinit(std.testing.allocator);
builder.appendSlice("Hel");
builder.appendSlice("lo");
builder.appendSingle(' ');
builder.appendSlice("Wo");
builder.appendSlice("rld");
builder.appendSingle('!');
try std.testing.expectEqualStrings("Hello World!", builder.toSlice());
try std.testing.expectEqual(12, builder.toSlice().len);
const my_slice = try builder.toSliceOwned(std.testing.allocator);
defer std.testing.allocator.free(my_slice);
try std.testing.expectEqualStrings("Hello World!", my_slice);
try std.testing.expectEqual(12, my_slice.len);
}