Semantics of undefined and default field values

std.Thread.Pool is defined with the following fields:

mutex: std.Thread.Mutex = .{},
cond: std.Thread.Condition = .{},
run_queue: RunQueue = .{},
is_running: bool = true,
allocator: std.mem.Allocator,
threads: []std.Thread,

The first four are provided with defaults, and the last two are provided in the Options struct when calling .init:

pub const Options = struct {
    allocator: std.mem.Allocator,
    n_jobs: ?usize = null,
    track_ids: bool = false,
    stack_size: usize = std.Thread.SpawnConfig.default_stack_size,
};

pub fn init(pool: *Pool, options: Options) !void {

In tests at the bottom of the file, it appears that std.Thread.Pool is intended to be initialized as follows:

var pool: Pool = undefined;
try pool.init(.{
    .allocator = std.testing.allocator,
});

It’s my understanding of the language semantics that undefined means that the variable is not in a well-defined state, so it shouldn’t be assumed that the fields were initialized to their default values. Is that not correct? If so, then isn’t this initialization pattern illegal behaviour?

1 Like

Wait. I see that in the init function those fields are being assigned implicitly on the pool.* = .{ line.

My mistake. I need more coffee

5 Likes

Because the current zig cannot reference the result position before return, this compromise is made.

I believe that if this issue is resolved, the std.Thread.Pool initialization API could be designed to directly return to the result location.

this is still a good example to show why using default values can be trap. The code here was written well, but if it hadn’t been, and it’s easy to see how it could have gone wrong, then I think your reasoning is correct.

1 Like

I don’t really use them much myself for reasons like this.

The main exception being config/option struct parameters; there I think they make the code a lot more readable and ergonomic to write.

2 Likes