Struct default member initialization

I don’t feel strongly about this, but would default struct member initialization be considered in Odin? I’ve been using the language for almost two years and it hasn’t been an issue, though it would be handy in some cases.

For example, I’ve used the wgpu package a lot, and having sensible defaults would smooth things out. I’ve also been trying Jai recently, and it’s nice knowing a struct is “ready to go” right away.

Curious if there are strong opinions either way.

1 Like

I wouldn’t mind it but you can create a struct that’s “ready to go” by having a proc which creates that struct and returns it.

Box :: struct {
	length, breadth, height: f32,
}

create_box :: proc() -> Box {
	return {1, 1, 1}
}

box := create_box()

You could create a wrapper over wgpu which has these create_* procs instead which does the initialization. I’m doing something similar for SDL3.

2 Likes

I do and have done this. Like I said, it’s not often I need constructors, but when you are working with large structs, or polymorphic ones, they can make life easier.

Odin’s zero-initialization is great, and I do make the zero value meaningful, but sometimes you want defaults that aren’t zero while still being able to override other fields. No need to “litter” the codebase with constructors if default struct member initialization was possible.

BlendMode :: enum {
    Opaque,
    Alpha,
    Additive,
}

PipelineDesc :: struct {
    depth_test:    bool,
    blend_mode:    BlendMode,
    sample_count:  i32,
    line_width:    f32,
    // ... many more members
}

create_pipeline_desc :: proc(
    depth_test: bool = true,
    blend_mode: BlendMode = .Opaque,
    sample_count: i32 = 1,
    line_width: f32 = 1.0,
    // ...
) -> PipelineDesc {
    return {
        depth_test,
        blend_mode,
        sample_count,
        line_width,
        // ...
    }
}

main :: proc() {
    pipeline := create_pipeline_desc(blend_mode = .Alpha)
}

With default struct member initialization, you could imagine something like the following. Essentially the same code in main, but you can check the struct definition to understand the defaults immediately. No need to find a constructor in the codebase. If you are using a library, you might not even know the constructor exists:

PipelineDesc :: struct {
    depth_test:    bool      = true,
    blend_mode:    BlendMode = .Opaque,
    sample_count:  i32       = 1,
    line_width:    f32       = 1.0,
    // ...
}

main :: proc() {
    pipeline := PipelineDesc{blend_mode = .Alpha}
}

The library writer must be a troll to not only not have a set prefix like create_* for all constructors but also go out of their way to put them, not above the struct, not below, but some other random place in the package. I wouldn’t mind such a feature though, but I just think the benefits would be marginal if the language is being used by sane people.

1 Like

I’m with you. I have a preference for it, but no strong feeling either way. Just wanted to hear what other people had to think.

Another weak pro for default initialization is grepping the code for Foo :: struct or LSP’s on-hover / goto-defintion will show the defaults immediately.

1 Like

One argument against it could be that, you cannot execute code in a default initializer, so in some cases you would have to create a proc. But now there can be 2 ways in a library to initialize structs which can both co-exist. How would you know which ones to use for which without searching through the library? The logical next step would be to allow some function to be called automatically when a struct is made which initializes it and well…

EDIT: I’m making some assumptions about how it would work and I don’t know how Jai does it. I’m not even really against constructors like in OOP languages. I just would like them to be opt-in rather than opt-out.

gingerBill has already talked about this on the discord server.
Here’s what he said:

Odin is a C alternative and having default field values like that requires an implicit constructor. And a constructor is a hidden cost which is non-trivial to implement.

1 Like

@RaphGL Thanks for that. I did a quick search through the discord and the github before I posted but couldn’t find anything.

@luma-nova in Jai, its super simple

#import "Basic";

Foo :: struct {
    a: int = 100;
    b: int;
    c: int;
}

main :: () {
    foo := Foo.{b = 20};
    print("%\n", foo);
}

Prints {100, 20, 0}


Having both default values and a procedure to construct the data can play together, but that might be stretching it’s usefulness.

// ...

create_foo :: (b: int) -> Foo {
    foo := Foo.{b = b};
    foo.a += 100;
    foo.c = do_something();
    return foo;
}

main :: () {
    // ...

    foo2 := create_foo(b = 65);
    print("foo2 = %\n", foo2);
    // prints "foo2 = {200, 65, 8}"
}
2 Likes

The more I think about this, the more I feel it would be a genuinely useful feature. Structs are just groups of bytes with semantic meaning - so why allow “invalid” objects by default? Odin already gives us zero-initialized and uninitialized options, which is great. But validity still isn’t guaranteed.

What I’m imagining is: a valid option means members with explicit defaults get their values, and everything else is zeroed. This matches current behavior and the best practice of designing zero to be a valid state wherever possible. Just like --- lets us opt into raw memory, there should be an explicit option to zero everything.

Apparently this pattern is called a NSDMI (non-static data member initializer). This video clued me into the name (it compares data initialization in Rust vs C++), and here is an article that talks about it’s implementation in C++.


I haven’t spent time yet thinking through the downsides, and I’m not sure how tricky this would be to implement or what it might break. But if the only change is that structs with explicit member defaults opt into this pattern, then existing all existing Odin code remains valid and behaves exactly as before. It is a purely additive feature. No breakage - but better ergonomics where you want them.

The “designing zero to be a valid state” is not very easy to follow in practice.

Take matrices (the zero matrix is not very useful as a transform, the identity would be a much better default) and quaternions (the zero quaternion is not a valid rotation) as examples that are built into the language but suffer from this issue.

“Try to make the zero value useful” is not necessarily the goal but just something that is useful. I always frame it as “try” rather than “must” because sometimes it isn’t a good idea.

As for matrices, defaulting to 0 is just as useful as defaulting f32 to 0 too, etc. I understand most people want the identity matrix a lot of the time, but the 0 matrix is still useful.

3 Likes

Bill, any thoughts on the idea of default member initialization? I initially made this post to query people’s opinions on this. Ended up convincing myself how nice it would be in certain situations. The nice part about it is backwards-compatibility with existing Odin code.

I don’t think it’s all that useful for core libraries, but custom/vendor libraries and C library bindings could take advantage of it.

Never going to happen because it would require implicit constructors in the generalized case.

1 Like