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.

ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ

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.

ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ

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

ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ

1 Like

ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ

3 Likes

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

I actually still send people that talk!

1 Like