Default values for comptime struct field

So i have a struct that has a field which type is decided at comptime. Can i assign default value to just one of the type cases?


fn getFieldType(comptime hasField: bool) type {
return switch (hasField) {
true => int,
false => void,
};
}

fn testStruct(comptime hasField: bool) type {
return struct {
OptionalField: getFieldType(hasField)
};
}

fn main () void {
const structWidthField: testStruct(true) = .{ .OptionalField = 1 };
const structWidthoutField1: testStruct(false) = .{ }; //this wont work
const structWidthoutField2: testStruct(false) = .{ .OptionalField = {} };
};

i want to be able to create to different structs like above, but without having to assign { } to the “unused” fields. if i give the optional field a defualt value, i have to give it to all the cases.

so basically, i want to be able to decide at comptime, if a struct has a field or not. If it has a field, i want it to require me to assign it at struct creation. If it has no field, i dont want to have to assign { } to the empty field.

is there anyway to achive this?

If you don’t mind generating the whole struct with or without the field, a straightforward way could be:

fn Gen(comptime O: ?type) type {
    return if (O) |T| struct { field: T } else struct {};
}

This would be easy if Zig lazily evaluated default values:

// NOTE: not a working solution

fn A(comptime has_field: bool) type {
    return struct {
        field: if (has_field) i32 else void = if (has_field)
            @compileError("must provide default") else void{},
    };
}

pub fn main() void {
    const a = A(false){};
    std.debug.print("a: {any}\n", .{a});

    // Compile error: "must provide default", despite providing value.
    // This would compile if Zig didn't evaluate the default value unless it had to.
    const b = A(true){ .field = 21 };
    std.debug.print("b: {any}\n", .{b});
}

I don’t know if this is something that Zig could/should do.

Anyway you may want to consider biting the bullet and just initializing the field to void, but here’s a hacky solution just for fun:

// Create two different `new` functions to wrap the initialization,
// and use `usingnamespace` to decide which definition to declare.
// (I don't really recommend doing this).

fn B(comptime has_field: bool) type {
    return struct {
        const Self = @This();
        foo: if (has_field) i32 else void,

        bar: u8,

        usingnamespace if (has_field) struct {
            fn new(foo: i32, bar: u8) Self {
                return .{ .foo = foo, .bar = bar };
            }
        } else struct {
            fn new(bar: u8) Self {
                return .{ .foo = {}, .bar = bar };
            }
        };
    };
}

pub fn main() void {
    const a = B(false).new(255);
    std.debug.print("a: {any}\n", .{a});

    const b = B(true).new(-10, 0);
    std.debug.print("b: {any}\n", .{b});
}
1 Like

It’s not super pretty but this works.

fn getFieldType(comptime hasField: bool) type {
    return switch (hasField) {
        true => i32,
        false => void,
    };
}

fn testStruct(comptime hasField: bool) type {
    return struct { OptionalField: getFieldType(hasField) = switch (getFieldType(hasField)) {
        i32 => 30,
        void => {},
        else => unreachable,
    } };
}

pub fn main() void {
    const structWidthField: testStruct(true) = .{ .OptionalField = 1 };
    const structWidthoutField1: testStruct(false) = .{}; // this now works
    const structWidthoutField2: testStruct(false) = .{ .OptionalField = {} };
}

Looks like this allows you to initialize a testStruct(true) without providing a value for optionalField. OP wants to disallow that.

Zig never ceases to amaze me:

fn Gen(comptime O: ?type) type {
    return if (O) |T|
        struct { field: T }
    else
        @Type(.{ .Struct = .{
            .is_tuple = false,
            .layout = .auto,
            .decls = &.{},
            .fields = &[1]std.builtin.Type.StructField{.{
                .name = "field",
                .type = void,
                .default_value = &{},
                .is_comptime = false,
                .alignment = 0,
            }},
        } });
}
5 Likes

Just recreate the type, pointing default_value to {} if it’s a void:

fn TestStruct(comptime hasField: bool) type {
    comptime var T = struct {
        field: if (hasField) i32 else void,
    };
    if (!hasField) {
        const st = @typeInfo(T).Struct;
        comptime var new_fields: [st.fields.len]std.builtin.Type.StructField = undefined;
        for (st.fields, &new_fields) |f, *nf| {
            nf.* = f;
            if (nf.type == void) {
                nf.default_value = &{};
            }
        }
        T = @Type(.{
            .Struct = .{
                .is_tuple = st.is_tuple,
                .layout = st.layout,
                .decls = &.{},
                .fields = &new_fields,
            },
        });
    }
    return T;
}
2 Likes

thanks for all the suggestions, i think @chung-leong solution looks most ideal.
but, what if i wanted to have struct funtions aswell, where would i put them?

Then you’re kinda screwed, since @Type() can’t add decls to a struct. There is no solution I can think of.

In my opinion, a void field should have an implicit default value of {}. That’s the only possible value so why make end-users specify it? The language already treats void differently (we don’t need to capture it when it’s returned by a function).

1 Like

oh well, thanks anyway. I agree about void default value.

my problem from the beginning was that i had several optional fields and it looked kind of bad when i wrote

const structWidthoutFields: testStruct(false, false, false) = .{
      .OptionalField1 = { },
      .OptionalField2 = { },
      .OptionalField3 = { },
 };

structWidthoutFields.DoSomething();

so i used @chung-leong solution and wrapped those fields in a struct, so now i have:

fn Options(comptime hasField1: bool, comptime hasField2: bool, comptime hasField3: bool) type {
        comptime var T = struct {
                OptionalField1: if (hasField1) i32 else void,
                OptionalField2: if (hasField2) i32 else void,
                OptionalField3: if (hasField3) i32 else void,
        };
        //set the void fields to default { }
        return T;
}

fn TestStruct(comptime hasField1: bool, comptime hasField2: bool, comptime hasField3: bool) type {
        Options: Options(hasField1, hasField2, hasField3)

        fn DoSomething() void { }
};

fn main() void {
        const structWidthoutFields: testStruct(false, false, false) = .{
              .Options = .{ },
         };

        structWidthoutFields.DoSomething();
}

So i have fewer lines and can still keep my struct function.