StringHashMap in a struct

Hello, I am trying to create a structure with a StringHashMap. What is the best way to init and deinit?

I am on the struggle bus big time and could use some help /tips-tricks…below is pseudo code and compile error

const std = @import("std");

pub const MyOtherThing = struct { awesome other things };

pub const MyThing = struct {
    name: []const u8,
    by_name: std.StringArrayHashMap,
};

pub init_MyThing(mem:Allocator) !MyThing {
    var thing:MyThing = . {
       .name = "boo-yeah",
       .by_name = undefined,
    }   


   ///error: expected type 'array_hash_map.ArrayHashMap([]const u8,MyOtherThing,array_hash_map.StringContext,true)', found '*hash_map.HashMap([]const u8,MyOtherThing,hash_map.StringContext,80)'
   things.by_name = try mem.create(std.StringHashMap(MyOtherThing));
   things.by_name.init(mem);
   
   put_MyOtherThing_in_MyThing(things, "cool-thing", init_MyOtherThing(...));
   
   return things;
  
}

pub put_MyOtherThing_in_MyThing(things:MyThing, thing_name:[] const u8, awesome_thing: MyOtherThing)  !void {
   try things.by_name.put(thing_name, awesome_thing); 
}


pub deinit_MyThing(mem:Allocator) !MyThing {
   ????
}

I am sure there is a simple concept I am missing. TY in advance!

Just quickly, you don’t want to call create – instead, call the .init method of StringHashMap, supplying your allocator as an argument. Then when you deinit your own struct, call the .deinit method of StringHashMap.

Hopefully that will get you going, sorry for the short reply.

4 Likes

The error here is this:
mem.create creates a pointer *StringArrayHashMap(...), but you don’t store a pointer in your struct, as you put by_name: std.StringArrayHashMap(MyOtherThing), without a *.
Generally it doesn’t make sense to store them as a pointer anyways, unless you want to share it with other structs.

The second problem is things.by_name.init(mem);. The init() function is not a member function, so it needs to be called like this
.by_name = std.StringArrayHashMap(MyOtherThing).init(mem)

Generally most data structures in the standard library follow this simple pattern:
To init: ...val = std.DataStructure(T).init(allocator)
To deinit: ...val.deinit()

Additionally I would advice against the C-style functions in favor of using member functions, which makes it easier to read/write (e.g. instead of deinit_MyThing(myThing), it would be myThing.deinit()) and it conforms to convention used by the standard library.
Here is your example fixed and rewritten in that style:

pub const MyThing = struct {
    name: []const u8,
    by_name: std.StringArrayHashMap,
    pub fn init(mem: Allocator) !MyThing {
        var thing: MyThing = . {
            .name = "boo-yeah",
            .by_name = std.StringHashMap(MyOtherThing).init(mem),
        }
        thing.put("cool-thing", MyOtherThing.init(...));
        return thing;
    }

    pub fn deinit(self: MyThing) void {
        var it = self.by_name.iterator();
        while(it.next()) |entry| {
            entry.value_ptr.deinit(); // Don't forget to deinit the entries of the hash map as well. This is not done automatically.
        }
        self.by_name.deinit();
    }

    pub fn put(self: *MyThing, thing_name: []const u8, awesome_thing: MyOtherThing) !void {
        try self.by_name.put(thing_name, awesome_thing);
    }
};
3 Likes

you don’t want to call create – instead, call the .init method of StringHashMap, supplying your allocator as an argument.

Ahh - that was the concept I had missed. Thank you thank you!!!

Generally most data structures in the standard library follow this simple pattern:
To init: ...val = std.DataStructure(T).init(allocator)
To deinit: ...val.deinit()

Wow, lol I had totally missed it. Aha, ok, I really appreciate this knowledge. Very helpful. Thank you so much for taking the time with an excellent example! I really appreciate your help.

Additionally I would advise against the C-style functions in favor of using member functions,

lol well spotted! Can do.

I was wondering…only from a place of curiosity / learning, no challenge… for you, what are the benefits of a member fn approach from your pov?

  • system → compile benefits?
  • style → maint / ease-of-use / ‘how we think’?

Thank you again!

1 Like

I would say it is almost entirely style / code-organization.
I think it helps people by:

  • having functions associated with a scope / type, that those functions are related to (grouping things explicitly instead of via naming convention)
  • it is convention/how most Zig code is written
  • method syntax can be more convenient because of scoped (and thus shorter) names
  • it helps code editors, because when you type my_instance. your editor can lookup what methods exist and suggest those to you (in theory that could be done for freestanding functions), . is a useful trigger character for a code editor to start trying to provide autocomplete/suggestions, because at that point the instance is already known so the possible completions are already a useful more narrow subset of everything that could be called/used

While the compiler probably wouldn’t care that much either way.

3 Likes

I am a believer!

I couldn’t get my code to work in a “C” style split of function from data because I haven’t fully grasped how zig manages memory.

However, as member functions, it worked as @IntegratedQuantum wrote (adjusting for bugs in my original example) and was cleaner.

1 Like