What is lifetime of non-variable?

By lifetime I mean when is it not undefined behavior to read memory?

Zig allows you to get address of non-variable like &1, &(2+4), &(x+1), &[3]i8{1,2,3}.

fn f0() void {
	var x: *const i8 = undefined;
	x = &1;
	// is x.* undefined behavior?
}

fn f1(arg: i8) void {
	var x: *const i8 = undefined;
	x = &(arg + 1);
	// is x.* undefined behavior?
}

fn f2(arg: i8) void {
	var x: *const i8 = undefined;
	{
		x = &(arg + 1);
	}
	// is x.* undefined behavior?
}

fn f3(arg: i8) *const i8 {
	return &(arg + 1);
}

fn f4() void {
	var x: *const i8 = undefined;
	x = f3(-1);
	// is x.* undefined behavior?
}
5 Likes

I’m not sure, but I don’t think it’s undefined. I think zig will place the constant in the data section and just get a pointer to it.

f0 is well-defined behavior. When the operand to & is a comptime-known constant, the pointer created has infinite lifetime, and refers to data in the binary’s global read-only data section

f3, as you probably expect, is undefined behavior. There is no way for this to work: even if the temporary value lived in the function’s stack frame for as long as possible, that pointer would become invalid once it returns.

f1 and f2 are a little trickier. In the current compiler implementation, both of these are valid; the operand to & is placed into a stack allocation which persists for the function’s entire duration. If I recall correctly from previous discussions with SpexGuy, the current plan for the language specification is that such temporaries are guaranteed to live for the duration of their containing scope; so f1 would be well-defined behavior, but f2 would be UB. However, I may be misremembering this.

In general, it would probably be desirable to avoid this kind of code in the first place (aside from the f0 pattern, which is somewhat common and completely reasonable, since having the value be comptime-known sidesteps the lifetime problem entirely).

10 Likes

the current plan for the language specification is that such temporaries are guaranteed to live for the duration of their containing scope

Confirmed undefined behavior outside scope:
pub fn main() void {
	var expect_at_x: usize = undefined;
	var x: *const usize = undefined;
	var y: *const usize = undefined;
	for(0..2) |i| {
		y = &(i*2);
		if((i&1) == 0){
			x = y;
			expect_at_x = x.*;
		}
	}
	//x.* is CONFIRMED undefined behaviour
	@import("std").debug.print("{} {}\n", .{
		x.*,
		expect_at_x
	});
	//2 0 printed
}

What I understand:

  • comptime(&(whatever)) is not undefined behaviour everywhere
  • &(variable+1) is not undefined inside scope but don’t write code like that