Overall I’ve been really enjoying Zig’s error handling system, but cleanup of objects hasn’t been great and I’m wondering if there are other approaches to solving this problem (or future language features planned):
Option 1 - duplicate destruction logic in init():
const Obj = struct {
a: A,
b: B,
pub fn init() Obj {
const a = try A.init();
errdefer a.deinit();
const b = try B.init();
errdefer b.deinit();
return .{ .a = a, .b = b};
}
pub fn deinit(obj: *Obj) void {
obj.a.deinit();
obj.b.deinit();
}
pub fn doStuff(obj:* Obj) void {
foo(obj.a);
obj.b.bar();
}
Option 2 - use optional types
const Obj = struct {
a: ?A = null,
b: ?B = null,
pub fn init() Obj {
var obj: Obj = .{};
obj.a = try A.init();
obj.b = try B.init();
return obj;
}
pub fn deinit(obj: *Obj) void {
if (obj.a) |a| a.deinit();
if (obj.b) |b| b.deinit();
}
pub fn doStuff(obj:* Obj) void {
foo(obj.a.?);
obj.b.?.bar();
}
Maybe unrealistic but I’m hoping for the best of both worlds:
- After init() completes successfully, we know that
Object
is in an initialized state so thatdoStuff
doesn’t need to use.?
- Init of
Object
can use struct initialization so that we know that all variables are being properly initialized. - Deinit code is only written once
Somewhat related, it’s interesting that Typescript can change the type of an expression through control flow, e.g. in theoretical Zig:
if (a != null) {
a.print(); // doesn't require .? as we know a is no longer optional
}
a.?.print(); // a is still optional on this path
The problem above is similar in that Object is in different states - partially constructed vs. fully constructed, so maybe there is a way ideas like that could apply.
Or maybe there is just a completely different pattern that I’m missing here that I should be using?