var foo: Foo = ...;
const bar = &blk: {
break :blk foo.bar;
};
bar.* = 2;
assert(bar == &foo.bar);
I think this is because of RLS: the & forwards a result location to break :blk foo.bar, so a copy is avoided.
Now, I believe inline fn is semantically similar to blocks. So why doesn’t this work?
var foo: Foo = ...;
const bar = &foo.getBar();
bar.* = 2;
In my head, the two should be equivalent, so RLS should still apply.
I know there has been discussion about why you can’t return a pointer-to-local from an inline fn, but I believe that’s because inline fns are like blocks, and you can’t return a pointer-to-local from those either. But this question is about RLS.
RLS doesn’t really fit here. The issue you are observing here is that Foo is passed by value/copy in getBar. So you can’t get a reference to the local value of bar in the inline function. If you change the signature to take a pointer, then you can return a pointer to the bar variable and update it.
const std = @import("std");
const log = std.log.scoped(.main);
const Foo = struct {
bar: u8,
qux: u16,
// Foo is copied. Can't get a pointer to a local stack variable, probably the reason for the compile error if you tried to return *u8
pub inline fn getBar(self: Foo) u8 {
return self.bar;
}
pub inline fn getBarPtr(self: *Foo) *u8 {
return &self.bar;
}
};
pub fn main() void {
var foo = Foo{ .bar = 1, .qux = 16 };
const bar = foo.getBarPtr();
bar.* = 6;
std.debug.assert(foo.getBar() == 6);
log.debug("Foo: {any}", .{foo}); // Logs `debug(main): Foo: .{ .bar = 6, .baz = 16 }`
}
I don’t think it’s accurate to say that RLS doesn’t fit here, because I believe the first example
const bar = &blk: {
break :blk foo.bar;
};
works because of RLS.
From the langref:
For instance, if the expression .{ .a = x, .b = y } has a result location of ptr, then x is given a result location of &ptr.a, and y a result location of &ptr.b.
So it stands to reason that in this example, without RLS, break :blk foo.bar would first create a temporary copy of foo.bar, then take the address of that. But this doesn’t happen because the block gets an result location of &bar.
I expect the inline fn to inline to basically the same thing as the block. So I don’t think it’s too unreasonable to expect RLS to take effect in this situation as well.
First of all, inlining is an optimization hint. Semantically, an inline function should not have any difference from a regular function. An inline function is not a macro, and the return result of a function is always an rvalue.
However, the block expression here surprises me quite a bit. It seems to consider that the result of a block expression can be passed as an lvalue. I am very doubtful whether this is an intentional design, a coincidence in the current implementation, or even a bug.
Alright, I understand that the wording in the document looks somewhat different, but I think the wording in the document is mainly to clarify that the inline keyword in C/C++ is merely an “optimization suggestion” that hardly has any effect in practice, while Inline functions in Zig will definitely cause actual inlining. However, this does not mean that inline functions have the effect of macros.
Honestly, to me this looks like a bug that shouldn’t be relied on. Sure, for this simple a case, bar == &foo.bar could very well be true, but more because of an optimization, rather than it being semantically assured.
The & operator should be taking the address of the expression directly following it. If that expression is an l-value (i.e. a variable or a struct/union member), then it takes the address of that l-value; otherwise, it takes the address of the expression’s result, a temporary. Crucially, a block is not an l-value expression and prepending an & operator should not be changing that fact.
Edit: In other words, I guess I mean that the RLS of the & operator should actually be that it introduces a temporary specifically in the case that the expression that follows it is anything other than an l-value. Otherwise, I would find the semantics confusing.
But again, if the compiler can prove that the temporary “physically” makes no difference, the address could be optimized to be the same as of the l-value inside of the block. But the result pointer should definitely be one to const data.
Edit: Last sentence semantics (realized after reading a comment on another thread :P)
There are other parallel discussions on this subject, and it’s an open question for blocks within in a function whether they can pass references to locals out of the block. I’ve been convinced that it’s ok that they can. There are use cases that can’t be done otherwise without adding a complex mechanism.
That inline functions are semantically functions, not some fancy reusable block or macro with different rules, appears to be much more set in stone.
That inline functions are semantically functions, not some fancy reusable block or macro with different rules, appears to be much more set in stone.
Adding the inline keyword to a function definition makes that function become semantically inlined at the callsite.
I think this ambiguity arrives from what exactly “semantically inlined” means.
I’ve been convinced that it’s ok that they can. There are use cases that can’t be done otherwise without adding a complex mechanism.
Why can’t you just not use a block? The only case I can think of is in contexts like while loop conditionals, but you probably shouldn’t be doing that anyway.
But that’s neither here nor there.
I’ve been assuming the only semantic difference between a non-inline and inline function is that the parameters of an inline function, that are not declared comptime, are treated as comptime if the argument passed is comptime-known in the caller.
Are there other differences?
Oh, another obvious one is that you can’t pass an inline function by name or take its address.