How to return an ArrayList after allocate in function?

When I try to return an ArrayList:

const std = @import("std");
const Allocator = std.mem.Allocator;

pub fn main() !void {}

fn notReturn(allocator: Allocator) !void {
    var list = std.ArrayList(i32).init(allocator);
    defer list.deinit();

    try list.append(1);
    try list.append(2);

    std.debug.print("List: {any}\n", .{list.items});
}

fn isReturn(allocator: Allocator) !std.ArrayList(i32) {
    var list = std.ArrayList(i32).init(allocator);
    defer list.deinit();

    try list.append(1);
    try list.append(2);

    std.debug.print("List: {any}\n", .{list.items});

    return list;
}

test "not return test" {
 
    try notReturn(std.heap.page_allocator);
}

test "return test" {
  
    const result = try isReturn(std.heap.page_allocator);
    std.debug.print("Return: {any}\n", .{result});
}

It compiles succesfully but when run it gives error:

zig build test
Test [1/2] test.not return test... List: { 1, 2 }
Test [2/2] test.return test... List: { 1, 2 }
Return: array_list.ArrayListAligned(i32,null){ .items = { Segmentation fault at address 0x
7feff8962000
/media/lucifer/DATA/IMPORTANT/zig-linux-x86_64-0.10.1/lib/std/fmt.zig:661:22: 0x21c18d in 
formatType__anon_4934 (test)
                for (value) |elem, i| {
                     ^
/media/lucifer/DATA/IMPORTANT/zig-linux-x86_64-0.10.1/lib/std/fmt.zig:595:87: 0x2187c2 in 
formatType__anon_4131 (test)
                try formatType(@field(value, f.name), ANY, options, writer, max_depth - 1)
;
                                                                                      ^
/media/lucifer/DATA/IMPORTANT/zig-linux-x86_64-0.10.1/lib/std/fmt.zig:188:41: 0x218426 in 
format__anon_4119 (test)
                .alignment = placeholder.alignment,
                                        ^
/media/lucifer/DATA/IMPORTANT/zig-linux-x86_64-0.10.1/lib/std/io/writer.zig:28:34: 0x215a7
0 in print__anon_3276 (test)
            return std.fmt.format(self, format, args);
                                 ^
/media/lucifer/DATA/IMPORTANT/zig-linux-x86_64-0.10.1/lib/std/debug.zig:93:27: 0x213b2c in
 print__anon_1477 (test)
    nosuspend stderr.print(fmt, args) catch return;
                          ^
/home/lucifer/test/src/main.zig:40:20: 0x213a98 in test.return test (test)
    std.debug.print("Return: {any}\n", .{result});
                   ^
/media/lucifer/DATA/IMPORTANT/zig-linux-x86_64-0.10.1/lib/test_runner.zig:63:28: 0x2168f3 
in main (test)
        } else test_fn.func();
                           ^
/media/lucifer/DATA/IMPORTANT/zig-linux-x86_64-0.10.1/lib/std/start.zig:604:22: 0x21447c i
n posixCallMainAndExit (test)
            root.main();
                     ^
/media/lucifer/DATA/IMPORTANT/zig-linux-x86_64-0.10.1/lib/std/start.zig:376:5: 0x213f81 in
 _start (test)
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
    ^
error: the following test command crashed:
/home/lucifer/test/zig-cache/o/4b94fda1f98d2f61fa2a145e0b04a6bd/test
error: test...
error: The following command exited with error code 1:
/media/lucifer/DATA/IMPORTANT/zig-linux-x86_64-0.10.1/zig test /home/lucifer/test/src/main
.zig --cache-dir /home/lucifer/test/zig-cache --global-cache-dir /home/lucifer/.cache/zig 
--name test --enable-cache 
error: the following build command failed with exit code 1:
/home/lucifer/test/zig-cache/o/8ab9fd52cdc07f23f15ce435a5b2c0ba/build /media/lucifer/DATA/
IMPORTANT/zig-linux-x86_64-0.10.1/zig /home/lucifer/test /home/lucifer/test/zig-cache /hom
e/lucifer/.cache/zig test

I think this is because the ArrayList is freed after exiting the function. How to fix it? My compiler version is 0.10.1.

Here:

fn isReturn(allocator: Allocator) !std.ArrayList(i32) {
    var list = std.ArrayList(i32).init(allocator);
    errdefer list.deinit(); // Use errdefer to deinit only on error
1 Like

You’re almost there. When you call the defer it will unconditionally deinit the ArrayList when the scope ends; int his case when the function ends. So you are returning a uninitialized ArrayList. To return the initialized ArrayList while properly uninitializing it in case of error in the function, use errdefer:

fn isReturn(allocator: Allocator) !std.ArrayList(i32) {
    var list = std.ArrayList(i32).init(allocator);
    errdefer list.deinit();

    try list.append(1);
    try list.append(2);

    std.debug.print("List: {any}\n", .{list.items});

    return list;
}

The caller of the function is now responsable for calling deinit on the returned ArrayList.

4 Likes

It’s generally a code smell to return an ArrayList as the return type of the function. It tends to work out better to accept a mutable ArrayList as a parameter, and have the function append to it.

4 Likes

Can you explain why returning an ArrayList is code smell?

1 Like

It’s usually advised to have a function do a single thing. Creating an Array list, and appending to it are two things.

You also want to reduce the mental load when reading the code. There’s a danger of a context switch If tomorrow you have to wonder how come your array was created with some items already in it…

That being said, a function that creates and populates an array(or any other struct) has a potential to simplify test cases, especially if many tests use the same data.

Although, in zig source code, I rarely see such a function. Instead, many tests have duplicate code. There’s pros and cons to everything, I suppose…

1 Like

The main reason it is considered to be a code smell is it can hide where allocations occur inside of a program.

You see this happen in C/C++ code a lot. You want to call a function because it seems to do what you need… then all of the sudden your program is performing horribly. Why is that? It’s because the program is continually calling malloc under the hood. If this happens in many places (which is often the case) then you may not be able to use that library for anything that needs more serious performance.

By passing an array as a mutable parameter (as @andrewrk suggested) you are shifting the burden of allocation to the caller of the function. This allows for finer grain control of where allocations occur and enables reusing pre-allocated memory.

4 Likes

Appending to an array list is a lot more flexible for the caller. The caller may already have an array list allocated and already have enough capacity for the results. Why bother making a new array, you can just put the answer in the one that already exists. Or, maybe the caller wants to store the results of many operations in the same array, so it appends the results, and remembers the index. Both of these patterns are possible when you accept the array list as a parameter, but they are not possible if it is the return type.

It’s also simpler for the function to append rather than create a new array - it’s strictly less logic.

In conclusion, it’s both more convenient for both the caller and the callee for the array list to be a parameter rather than a return value.

6 Likes

Thank you for your support, all comments are very helpful!

1 Like