Cleanup of object variables

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 that doStuff 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?

Not tested but I think you can do this:

const a = try A.init();

return .{
    .a = a,
    .b = B.init() catch |err| {
        a.deinit();
        return err;
    },
};

I don’t think this scales up well as variable count increases:

const a = try A.init();
const b = B.init() catch |err| {
    a.deinit();
    return err;
};
const c = C.init() catch |err| {
    b.deinit();
    a.deinit();
    return err;
};

return .{ .a = a, .b = b, .c = c };

Option 1 seems fine to me.

However, it can become a problem with result location semantics, though, i.e when something stores a pointer to a or b (since the return value’s a field will be a copy of the stack allocated a; the stack allocated a will go out of scope). I believe the relevant proposal for that is result location: ability to refer to the return result location before the `return` statement · Issue #2765 · ziglang/zig · GitHub

Option 1 is what i typically do, but still isn’t great for having to duplicate the cleanup logic (which is what I’m hoping there would be a better solution for), and I have hit the result location problems with it and the end result is that I allocate separate objects more often than I would in C/C++.

Building on some of the ideas in issue #2765 and possibly giving up on error catching for uninitialized variables… something like this could be interesting (bikeshedding syntax issues aside):

   a: A = undefined,
   b: B = undefined,

   pub fn init() Obj {
      return |*obj| {
        obj.a = try A.init();
        errdefer obj.a.deinit();
        obj.b = try B.init();
        errdefer obj.b.deinit();
        errdefine deinit(obj);   // creates a new function based on the errdefer logic at this point
        return obj;
      }
   }

Rust’s cc was that anything larger than 2 registers for the return became an implicit pointer to in as the first parameter. So returning a value was pretty cheap.