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 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
2 Likes