Best way to manage list of strings

Hi,
I am trying to work with lists of strings, that will be processed, with new lists being generated in every iteration. Here’s what I use to generate the list:

pub fn generate(allocator: std.mem.Allocator, prng: *std.rand.DefaultPrng, stringLength: u16, numStrings: u32) ![]*[]const u8 {
    var stringArray: []*[]const u8 = try allocator.alloc(*[]const u8, numStrings);
    var i: u32 = 0;
    while (i < numStrings) {
        var binaryString = try allocator.create(u8, stringLength);
        var c: u32 = 0;
        while (c < stringLength) : (c += 1) {
            binaryString[c] = prng.random().intRangeAtMost(u8, '0', '1');
        }

        stringArray[i] = &binaryString;
        i = i + 1;
    }
    return stringArray;
}

This mostly works unless you start processing strings in the array; then it starts to break at random moments (processing involves duplicating the array, making some changes, and placing the result in yet another array of strings).

I guess this implies a memory leak somewhere, and I’m pretty sure I’m getting everything wrong about memory allocation. I’d be grateful for any explanation.

1 Like

Hi, welcome to the forum!
Here’s how I’d do it:

const std = @import("std");

pub fn generate(allocator: std.mem.Allocator, random: std.rand.Random, string_len: u16, num_strings: u32) ![][]const u8 {
    var strings = try allocator.alloc([]u8, num_strings);
    for (0..num_strings) |i| {
        strings[i] = try allocator.alloc(u8, string_len);
        for (0..string_len) |j| {
            strings[i][j] = random.intRangeAtMost(u8, '0', '1');
        }
    }
    return strings;
}

test generate {
    const allocator = std.testing.allocator;

    var prng = std.rand.DefaultPrng.init(blk: {
        var seed: u64 = undefined;
        try std.os.getrandom(std.mem.asBytes(&seed));
        break :blk seed;
    });

    const strings = try generate(allocator, prng.random(), 5, 10);

    for (strings, 0..) |string, i| {
        std.debug.print("String #{d}: {s}\n", .{ i, string });
    }

    for (strings) |string| {
        allocator.free(string);
    }

    allocator.free(strings);
}
2 Likes

Here’s a working program. See below for some notes.

const std = @import("std");

pub fn generate(
    allocator: std.mem.Allocator,
    prng: *std.rand.DefaultPrng,
    stringLength: u16,
    numStrings: u32,
) ![][]u8 {
    const strings: [][]u8 = try allocator.alloc([]u8, numStrings);

    // An error may occur at any point, so we make sure to
    // free what we have up till that point.
    var allocated: usize = 0;
    errdefer {
        for (0..allocated) |i| allocator.free(strings[i]);
        allocator.free(strings);
    }

    // The *str capture gives you mutable access.
    for (strings) |*str_ptr| {
        str_ptr.* = try allocator.alloc(u8, stringLength);
        allocated += 1;

        // Same here, *b can be mutated directly.
        for (str_ptr.*) |*byte_ptr| {
            byte_ptr.* = prng.random().intRangeAtMost(u8, '0', '1');
        }
    }

    return strings;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var prng = std.rand.DefaultPrng.init(0);

    const strs = try generate(allocator, &prng, 8, 3);
    defer {
        for (strs) |str| allocator.free(str);
        allocator.free(strs);
    }
    for (strs) |str| std.debug.print("{s}\n", .{str});
}
  • You can use the * in a for loop capture to get a pointer to the item and thus be able to modify it directly.
  • Allocating with alloc returns a slice of the type you specify, so for u8 it’s []u8 and not []const u8. So I set the return type to [][]u8 accordingly.
  • Every time you alloc or create, you have to think about possible errors occurring mid-flight, so you have to devise an errdefer strategy to free the already allocated items.
1 Like

Thanks a lot for the answer. I see you’re using snake_case where I used CamelCase. Would that be the “ziggish” way?

1 Like

You’re welcome! And yeah, it is actually! I’m so used to it that can’t help maintaining it everywhere :sweat_smile: Here’s the “Names” section of the official style guide.

2 Likes

It’s also worth pointing out that if you follow the style guide, you’ll get better lsp support. ZLS current highlights things based on that convention :slight_smile:

2 Likes