Just to go the extra mile: this will leak memory on certain error conditions. For example, if the words_arr.append
call within the loop fails, then the new_str
memory returned from handleStr
will not be freed.
The fix for that is pretty simple: errdefer
for (hello_arr) |str| {
const new_str = try handleStr(str, allocator);
errdefer allocator.free(new_str);
try words_arr.append(new_str);
}
Note, though, that this class of problems can be tested for by using std.testing.checkAllAllocationFailures
. See
for details on what it’s for/how it works, but here’s a quick adaptation of your code to use it:
const std = @import("std");
fn handleStr(str: []const u8, allocator: std.mem.Allocator) ![]const u8 {
var new_str = std.ArrayList(u8).init(allocator);
errdefer new_str.deinit();
for (str) |char| {
try new_str.append(char);
}
return try new_str.toOwnedSlice();
}
test "leaks" {
try std.testing.checkAllAllocationFailures(std.testing.allocator, testImpl, .{});
}
const hello_arr = [_][]const u8{ "howdy", "hello", "waddup" };
fn testImpl(allocator: std.mem.Allocator) !void {
var words_arr = std.ArrayList([]const u8).init(allocator);
errdefer words_arr.deinit();
for (hello_arr) |str| {
const new_str = try handleStr(str, allocator);
errdefer allocator.free(new_str);
try words_arr.append(new_str);
}
const new_str_arr = try words_arr.toOwnedSlice();
defer allocator.free(new_str_arr);
for (new_str_arr) |str| {
std.debug.print("{s}\n", .{str});
allocator.free(str);
}
}
With the errdefer
in the loop, we still hit a leak (truncated to only include the most relevant lines of the stack trace):
fail_index: 2/4
allocated bytes: 136
freed bytes: 131
allocations: 2
deallocations: 1
allocation that was made to fail:
C:\Users\Ryan\Programming\Zig\tmp\leaks.zig:8:27: 0xfc10bd in handleStr (test.exe.obj)
try new_str.append(char);
^
C:\Users\Ryan\Programming\Zig\tmp\leaks.zig:25:38: 0xfc17e8 in testImpl (test.exe.obj)
const new_str = try handleStr(str, allocator);
^
1/1 leaks.test.leaks...FAIL (MemoryLeakDetected)
C:\Users\Ryan\Programming\Zig\zig\lib\std\testing.zig:1103:21: 0xfc2801 in checkAllAllocationFailures__anon_2808 (test.exe.obj)
return error.MemoryLeakDetected;
^
C:\Users\Ryan\Programming\Zig\tmp\leaks.zig:15:5: 0xfc2afc in test.leaks (test.exe.obj)
try std.testing.checkAllAllocationFailures(std.testing.allocator, testImpl, .{});
^
[gpa] (err): memory address 0x23dc3150000 leaked:
C:\Users\Ryan\Programming\Zig\tmp\leaks.zig:8:27: 0xfc10bd in handleStr (test.exe.obj)
try new_str.append(char);
^
C:\Users\Ryan\Programming\Zig\tmp\leaks.zig:25:38: 0xfc17e8 in testImpl (test.exe.obj)
const new_str = try handleStr(str, allocator);
^
This is a bit confusing because it shows the allocation that was made to fail and the allocation that leaked as the same code, but the problem is:
- After a successful
words_arr.append
, there is a heap-allocated slice in the words_arr
ArrayList
- The slices within the
words_arr
ArrayList are only freed at the end of the test in the for (new_str_arr)
loop which only runs when there are no errors.
So, the fix is to always free the slices that successfully get added to the ArrayList by moving the free
into a defer. I’m going to make a few changes here, since the new_str_arr
is also not necessary (you likely just want to access words_arr.items
).
var words_arr = std.ArrayList([]const u8).init(allocator);
defer {
for (words_arr.items) |str| {
allocator.free(str);
}
words_arr.deinit();
}
for (hello_arr) |str| {
const new_str = try handleStr(str, allocator);
// Only free on error; if the append call succeeds then the defer
// above will take care of it
errdefer allocator.free(new_str);
try words_arr.append(new_str);
}
// ...
And with that, the test passes when using checkAllAllocationFailures
and we can be reasonably confident that we’re handling memory correctly even in the face of allocation failures.