Lifetime and scope of a const variables

Hello,
const x = 1; Is it an immutable constant Y / N?

If Y, then why: ridx is a constant

for (vpnl.label.items, 0..) |l, idx| {
    const ridx = usizeToStr(idx);

    if (l.text.len > 40) grd.addRows(Origine, &.{ ridx, l.name, l.text[0..39] })
    else grd.addRows(Origine, &.{ ridx, l.name, l.text });
}

discovered this while compiling with version 0.12.0 dev.

This doesn’t make ridx a constant, but it means that ridx is an immutable variable in each iteration of the loop. Y / N

const in Zig doesn’t necessarily mean a value is constant for the lifetime of program execution or for all eternity, it means a value is constant for its lifespan, which is dictated by lexical scope.

That’s correct, but to be pedantic, “immutable variable” is an oxymoron, hence const. There may be confusion based on how the words “constant” and “variable” are used in other languages or fields.

5 Likes

I am guessing that usizeToStr(idx) returns a []const u8?

This makes me question what is going on here (your code doesn’t show us) either usizeToStr uses some kind of buffer or allocator that has a longer lifetime than the call to usizeToStr or you are using memory that is local to usizeToStr which means that the returned string is always invalid by the time it is received (plus minus some “luck” where the memory isn’t immediately overwritten by somebody else using it for something new)

[]const u8 is a slice, which is essentially a pointer and a length, my guess is you end up with const ridx where ridx.ptr is pointing to stack memory that no longer belongs to you.

I think you should pass a buffer or an allocator to usizeToStr and change the function to store the result of the conversion in the buffer or allocate memory for it with the allocator, or better just use std.fmt.bufPrint or std.fmt.allocPrint instead.

However you still need something to take ownership of the resulting string, judging from l.name and l.text just being passed to addRows your grd rows probably only contain string slices that are either constants embedded into your executable for example string literals or other referenced strings that often just happen to have a long enough lifetime by being owned by some other datastructure.
One way would be to add an ArrayList to grd that holds all the strings that are dynamically allocated and then add allocated strings to that list when you allocate one and free those when the grd is destroyed, but that may be a lot of manual work.

Another way would be to make the addRows function copy every string given to it for example into an arena and then free that when it is destroyed, that way you don’t have to distinguish between static/global lifetimes, however I am unsure about the arena, because if you have trouble managing lifetimes it could make things to appear like they are working, when in reality you are just leaking memory and haven’t noticed.

I think until you are comfortable managing memory manually you probably should avoid arenas and instead make sure to use the GeneralPurposeAllocator to make sure you detect leaks.

Anyways with the info you have given, we are missing a lot of context, it is always easier to help if you provide us with a small program that illustrates what you are doing, because then it is way easier to point out simple errors like use of a temporary value outside its scope.

So here I made up a minimal example, with your code rewritten:

const std = @import("std");

const Label = struct {
    name: []const u8,
    text: []const u8,
};

const SomeDataStructure = struct {
    label: std.ArrayList(Label),

    fn init(allocator: std.mem.Allocator) SomeDataStructure {
        return .{
            .label = std.ArrayList(Label).init(allocator),
        };
    }
    fn deinit(self: *SomeDataStructure) void {
        self.label.deinit();
    }
};

const Strings = std.ArrayList([]const u8);

fn Grid(comptime Columns: comptime_int) type {
    return struct {
        const Self = @This();

        pub const Row = [Columns][]const u8;
        pub const Rows = std.ArrayList(Row);

        columns: Row,
        rows: Rows,
        dynamic_strings: Strings,

        pub fn init(allocator: std.mem.Allocator, columns: Row) Self {
            return .{
                .columns = columns,
                .rows = Rows.init(allocator),
                .dynamic_strings = Strings.init(allocator),
            };
        }
        pub fn deinit(self: *Self) void {
            self.rows.deinit();
            for (self.dynamic_strings.items) |string| {
                self.dynamic_strings.allocator.free(string);
            }
            self.dynamic_strings.deinit();
        }

        pub fn addRow(self: *Self, row: Row) !void {
            try self.rows.append(row);
        }

        // helper method
        fn printRow(row: Row) void {
            for (row) |str| {
                std.debug.print("| ", .{});
                const short_str = if (str.len > 40) str[0..40] else str;
                std.debug.print("{s: <40} ", .{short_str});
            }
            std.debug.print("|\n", .{});
        }

        pub fn print(self: *const Self) void {
            printRow(self.columns);
            for (self.rows.items) |row| printRow(row);
        }
    };
}

fn populateGrid(allocator: std.mem.Allocator, grid: *Grid(3), vpnl: SomeDataStructure) !void {
    for (vpnl.label.items, 0..) |l, idx| {
        const ridx = try std.fmt.allocPrint(allocator, "{d}", .{idx});
        errdefer allocator.free(ridx);
        try grid.dynamic_strings.append(ridx);

        // const short_text = if (l.text.len > 40) l.text[0..39] else l.text;
        // 0..40 is a range [0 to 40) where 0 is included and 40 is excluded
        // so there is no need to subtract 1
        const short_text = if (l.text.len > 40) l.text[0..40] else l.text;
        try grid.addRow(.{ ridx, l.name, short_text });
    }
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    defer {
        switch (gpa.deinit()) {
            .leak => @panic("leaked memory"),
            else => {},
        }
    }

    var vpnl = SomeDataStructure.init(allocator);
    defer vpnl.deinit();

    try vpnl.label.append(.{ .name = "Oolong Tea", .text = "Half fermented green tea giving it a rich flavor..." });
    try vpnl.label.append(.{ .name = "Cheap Supermarket Tea bags", .text = "this is the crap powder full with dirt and other stuff that remains when the good tea is cleaned from dirt" });
    try vpnl.label.append(.{ .name = "short", .text = "this is short" });

    var grid = Grid(3).init(allocator, [_][]const u8{ "index", "name", "short text" });
    defer grid.deinit();

    try populateGrid(allocator, &grid, vpnl);

    grid.print();
}

Output:

| index                                    | name                                     | short text                               |
| 0                                        | Oolong Tea                               | Half fermented green tea giving it a ric |
| 1                                        | Cheap Supermarket Tea bags               | this is the crap powder full with dirt a |
| 2                                        | short                                    | this is short                            |
2 Likes

Roughly, that’s what I’m doing. It was mainly to get confirmation of what I had understood. Thank you for taking the time to respond. P.S.: We are on the same wavelength.

1 Like