Trouble concatenating strings using an allocator

Hello Everyone,

newbie here coming to zig from a high level language like python and I am trying to concatenate strings at the runtime using an allocator and have the below function, however the call is running into error{OutOfMemory}![]u8.

const std = @import("std");

fn cocat_strings(str1: []const u8, str2: []const u8) u8 {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    defer _ = gpa.deinit();
    const combined = std.mem.concat(allocator, u8, &.{ str1, str2 });
    defer allocator.free(combined);
    return combined;
}

the caller looks like below

pub fn main() void {

    const name: *const [3:0]u8 = "Som";
    cocat_strings(name, name);  // looking for a result like "SomSom"
}

Any help with some explanation would be really appreciated.
Also, if someone can outline a few good resources on strings in zig that will be great.

Cheers,
DD.

In cocat_strings you use defer allocator.free(combined); that is executed when the scope (your function) exists.
What is expected is that you return the string you have just created. Your function should look something like:

fn concat_strings(str1: []const u8, str2: []const u8) []const u8 {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    const combined = std.mem.concat(allocator, u8, &[_][]const u8{ str1, str2 }) catch unreachable;
    return combined;
}
2 Likes

in addition to this I can also take an allocator: std.memory.Allocator as an argument and free and deinit outside of the function.
Your solution has worked , cheers.

Yeah, you can. Or you do your module to handle strings and you initialize an allocator for the whole module.
Strings are a good exercise for newbies like us to to get acquainted to the language. Start with:

pub const String = struct {
    buff: []u8,

    pub fn init(buff_size: usize) String {
    }
}

and write hundreds of functions to handle strings: concatenation, converting to and from numbers, to and from cstrings, writing to and from file … for some you will want to use allocator.realloc.

Generally don’t do this.

2 Likes

The return type of std.mem.concat is (in this case) error{OutOfMemory}![]u8. You declare combined as the direct result of this function call, meaning the type of combined is error{OutOfMemory}![]u8. You then pass combined to free, but that fails because free doesn’t expect an error union like that.

You should handle the possible error from calling concat by either using catch to handle the error directly, try to return the error (equivalent to catch |err| return err), or use if/else to conditionally handle the error. Likely you want to use try.

5 Likes

If you’re doing a bunch of string manipulation it’s a good use case for arena allocation. It will help you reduce how many resources you are juggling from many to 1. Start with just one big arena for everything - you can initialize it in main(), and then if you end up needing to free any of that stuff before program exit, then you can introduce more memory management. If you’re just making a CLI tool you probably don’t need anything beyond a single arena allocator for 100% of your memory allocations over the life of the entire program.

7 Likes

Hey :), This is the corrected version of your program, the first thing that has changed is the signature of the function, although not mandatory, it’s usually recommended for both clarity and consistency to have each function that requires memory allocation to accept an allocator parameter, even if you don’t care about being idiomatic, it’s still convenient to be able to pass any allocator.

Second the function signature was incorrect, you see concat can return an error, and a string in Zig is just a slice of u8, therefore the correct return type should be ![]u8, lastly in the main function I show you one way you can transform a ![]u8 into a []u8 which is done by catching the error path and handling it.

const std = @import("std");

fn cocat_strings(allocator: std.mem.Allocator, str1: []const u8, str2: []const u8) ![]u8 {
    return try std.mem.concat(allocator, u8, &.{ str1, str2 });
}

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

    const allocator = gpa.allocator();

    const name = "Som";
    const my_string = cocat_strings(allocator, name, name) catch |err| {
        return std.debug.print("Fatal : {}\n", .{err});
    };
    defer allocator.free(my_string);
    std.debug.print("result : {s}\n", .{my_string});
}
2 Likes

If you’re okay with potentially losing non-printable characters, you can use std.fmt.allocPrint:

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

  const name: []const u8 = "Som";
  const name_concat = try std.fmt.allocPrint(allocator, "{s}{s}", .{name, name});
  defer allocator.free(name_concat);

  std.log.info("Concatenated name: {s}", .{name_concat});
}
1 Like

Hey Andrew,

thanks for the reply, I was thinking on the same line and end up with GPA being the base allocator and arena being the main allocator like below. I did not share it here before as I do not want to confuse newbies like me.

const std = @import("std");

fn cocat_strings(allocator: std.mem.Allocator, str1: []const u8, str2: []const u8) error{OutOfMemory}![]const u8 {
    return try std.mem.concat(allocator, u8, &.{ str1, str2 });
}


pub fn main() !void {
    const name: *const [3:0]u8 = "Som";
    const direct_slice: []const u8 = "Test";
    const name_slice = name[0..];


    // create base allocator to do actual work
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const base_allocator = gpa.allocator();

    // create arena allocator to reuse the same memory
    var arena = std.heap.ArenaAllocator.init(base_allocator);
    defer _ = arena.deinit();
    const allocator = arena.allocator();

    var combined = try cocat_strings(allocator, name, name);
    std.debug.print("concat string: `{s}`\n", .{combined});

    combined = try cocat_strings(allocator, name_slice, direct_slice);
    std.debug.print("concat2 string: `{s}`\n", .{combined});
}

Cheers,
DD.

concat(allocator, u8, name, name) is shorter than cocat_strings(allocator, name, name). You could just const concat = std.mem.concat; rather than defining the cocat_strings function.

3 Likes

Come on. Don’t give a newbie a catch unreachable. Nobody should use that feature until they actually know what they’re doing.

5 Likes

Agreed. It’s also clearly and obviously incorrect here.

Shoutouts to myself seven years ago

2 Likes

You’re right, I admit.
Should have ![]const u8 as return type. Allocation may fail due to bugs, hardware problems, OS not to keep its promise, …

1 Like

Hope your metric applies to me :grinning_face:
Excited to know that in 7 years I will be sharp.
Reading the quality ziggit answers make feel pretty dumb sometimes (most of the time ?).

3 Likes

You can also just run out of memory if you use it all.

I actually still send people that talk!

1 Like