I tried playing around with providing a single valued error to a data structure and I think this is the most stable way to do it:
fn isErrorValue(comptime E: type) bool {
return switch (@typeInfo(E)) {
.ErrorSet => true, else => false,
};
}
pub fn MakeStruct(comptime error_value: anytype) type {
if (comptime !isErrorValue(@TypeOf(error_value))) {
@compileError("Parameter must have parent type ErrorSet.");
}
return struct {
const ErrorType = @TypeOf(ErrorValue);
const ErrorValue = error_value;
pub fn myCreate(_: @This(), comptime T: type, alloc: Allocator) ErrorType!*T {
return alloc.create(T) catch ErrorValue;
}
fn errorValue(_: @This()) ErrorType {
return error_value;
}
};
}
const MyError = error { MyOOM };
pub fn main() !void {
const foo = MakeStruct(MyError.MyOOM){};
// compiles fine...
const x = try foo.myCreate(usize, std.heap.page_allocator);
// still works fine...
defer std.heap.page_allocator.destroy(x);
// we get the right error
std.debug.print("\n{s}\n", .{ @errorName(foo.errorValue()) });
}
I tried being clever with @errorFromInt
but that actually plucks values out of the global error set. The MyError.MyOOM
value turned out to be 11, which is definitely unstable unless you’re grabbing that value directly.
Anyhow, to @IntegratedQuantum… you mentioned not wanting to rewrite half the standard library (lol) so I was wondering what it would take to make mirrored structures (just to see what the overhead for that would be)…
I analyzed array_list.zig
and I actually think there is a way out here. It’s actually quite easy. Check this out…
Probably a good function to start with:
pub fn ensureTotalCapacityPrecise(self: *Self, new_capacity: usize) Allocator.Error!void {
if (@sizeOf(T) == 0) {
self.capacity = math.maxInt(usize);
return;
}
if (self.capacity >= new_capacity) return;
// Here we avoid copying allocated but unused bytes by
// attempting a resize in place, and falling back to allocating
// a new buffer and doing our own copy. With a realloc() call,
// the allocator implementation would pointlessly copy our
// extra capacity.
const old_memory = self.allocatedSlice();
if (self.allocator.resize(old_memory, new_capacity)) {
self.capacity = new_capacity;
} else {
const new_memory = try self.allocator.alignedAlloc(T, alignment, new_capacity);
@memcpy(new_memory[0..self.items.len], self.items);
self.allocator.free(old_memory);
self.items.ptr = new_memory.ptr;
self.capacity = new_memory.len;
}
}
First, the return type. But if you gave it a second parameter that it can be customized with at the type definition level like…
const whatever = MyArrayList(T, my_error)...
And then save that error type like in the example I provided above…
const ErrorValue = error_value;
const ErrorType = @TypeOf(error_value);
You could then text replace all instances of Allocator.Error
with ErrorType
so all the return types are correct.
Okay, now the fun part - handling the allocator stuff. I did a search for try allocator
and only found 3 matches in the entire array_list.zig file.
You could go to those places, write a catch return ErrorValue
instead of try
. Now, they won’t be directly assignable with other ArrayList
s, but that may not be a bad thing actually… however since you can get the ArrayList.items
field, you can just pass the slice around if you need to view the data - maybe even qualify the child type as const while we’re at it.
I will say there’s one complication with the UnmanagedArrayList
… that version uses temporary Managed
array lists - so those call sights would need the extra comptime value parameter passed to their definitions. So that said, there’s a little more work to support unmanaged versions of things but if you just want one version, it’s easy.
Honesty, not bad all in all if you’re just trying to replace allocator errors (at least in this one case).