Lifetime of temporaries

Is the following program legal according to there rules of the language?

const std = @import("std");

fn f() u32 { return 92; }

pub fn main() void {
    const x = &f();
    std.debug.print("{}", .{x.*});
}

I can give at least three different answers:

A) No, because f() stores result in a temporary which is logically destroyed by the time the evaluation of const x = ... finishes.
B) Yes, because this is C++/Rust temporary lifetime extension
C) Yes: C++ and Rust need to restrict anonymous temporaries to full expression and the corresponding “temporary lifetime extension” carve-out because they have destructors. In general, compiler won’t be able to re-order calls to destructors, so a language-level rule for dropping temporaries early is required. Zig doesn’t have destructors, so, in Zig, all temporaries live up to at least the enclosing scope, and it’s the optimizer’s job to figure out when two temporaries can share the same storage safely.

In real code, this currently comes up fairly frequently when using AnyReader/AnyWriter, which like to close over a temporary generic reader:

const any_reader = file.reader().any();
// Is using any_reader safe here? Or is the .reader() already dead?

I am failing to find the answer to this question in the reference, but this sounds like something rather important to have documented

3 Likes

This would be illegal even in C++, because lifetime extension only applies to references, not pointers.
We bind lifetimes to lexical scope because once the variable goes out of scope, no one can refer to it, so that space is free for grabbing. In the case of a temporary, once you reach the end of the statement, no one can refer to it, so the same rule applies. Even though the compiler is unlikely to clean up that space in the middle of a function, it is allowed to reuse that space for other variables.
The context in the file reader is just a pointer. When creating the AnyReader, it simply copies the pointer and adds the vtable, there is no reference to the reader itself. This trick can be implemented for any reader whose context is pointer-sized or less.

1 Like

I think this is more tricky. What happens there is:

const generic_reader = file.reader();
const any_reader = generic_reader.any_reader();

any_reader stores a pointer to the generic_reader, not to the underlying File:

pub const Reader = io.Reader(File, ReadError, read);

pub fn reader(file: File) Reader {
    return .{ .context = file };
}

pub inline fn any(self: *const Reader) AnyReader {
    return .{
        .context = @ptrCast(&self.context), // <- pointer!
        .readFn = typeErasedReadFn,
    };
}

in other words, any_reader stores a pointer to a file descriptor, but that pointer points not to the File, but to the intermediate Reader which holds a (temporary) copy of fd.

1 Like

You’re right. I think that code is illegal.

I was also interested on knowing this and I got an answer on the zig discord and this code is correct for now and will likely be formalized as defined behavior.

referenced rvalues (temporaries) live until the end of the function, or until the end of the innermost loop they were in

3 Likes