It is. Weâre doing one more copy than would be strictly necessary by a swap, but that is a limitation of x86, as it doesnât have a mov memory to memory.
The typebrace test passes, but dotbrace doesnât. A typed initializer creates an intermediate value, while an unnamed initializer writes each field directly to the result location in order. This is documented further in the Result Locations section of the docs (it also provides a similar example to the test in the OP):
Expression
Result Location
Sub-expression Result Locations
.{x}
ptr
x has result location &ptr[0]
.{ .a = x }
ptr
x has result location &ptr.a
T{x}
ptr
x has no result location (typed initializers do not propagate result locations)
T{ .a = x }
ptr
x has no result location (typed initializers do not propagate result locations)
These subtleties are definitely prone to error, so Iâm not necessarily defending the status quo and would personally like to see this addressed before 1.0. However, for now, a good rule of thumb is that if you need to swap two values in one line, you should make sure youâre using either the T{ b, a } or @as(T, .{ b, a }) forms and not a bare .{ b, a }.
This was in depth knowledge I did not expect to receive but I am not complaining
const std = @import("std");
test "typed" {
var a: u1 = 0;
var b: u1 = 1;
a, b = struct { u1, u1 }{ b, a };
try std.testing.expectEqualSlices(u1, &.{ 1, 0 }, &.{ a, b });
}
test "infered" {
var a: u1 = 0;
var b: u1 = 1;
a, b = .{ b, a };
try std.testing.expectEqualSlices(u1, &.{ 1, 0 }, &.{ a, b });
}
Thats wild, on master as of today, typed passes but infered fails. Idk how I feel about that.
I honestly wish they would either just make it copy all of it and then maybe one day optimize as much on that behaveior as possible without the user noticing or say âits not happening all at once and we wont pretend that it isâ, and document that nicely.
Note that this is one of many examples of flaws with T{ ... } syntax (it canât support RLS for subtle reasons relating to pointer coercions), which is why thereâs an open proposal to remove it.
Thank you, I thought that const t = T{...}; and const t: T = .{...}; are exactly the same.
Could you please provide a bit more details or an example where RLS does not work?
const T = struct { f32 };
const S = struct { f64 };
fn foo() void {
const x: S = T{ expr };
_ = x;
}
âŚwhere expr is just some valid expression. This code is valid, because tuples coerce element-wise, and f32 can coerce to f64, so T can coerce to S. For RLS to work here, we start with a *S pointing to the stack location reserved for x; and we want to turn this into a *f32, probably going through a *T in the middle, so that expr can use that *f32 as its result location.
However, we canât do that. A value of type f64 canât be defined just by writing an f32 into memory, so likewise, we cannot define an S by writing a T into memory.
Funnily enough, since weâve recently removed anonymous struct types from the language, there are no types with named fields that are distinct but coercible. That means, I think, that struct (rather than array) initializations (i.e. S{ .x = expr }) probably could forward result locations. However, having this be the case for struct initializations but not array initializations would be an inconsistent design.
The other issues with T{ ... } syntax (unrelated to RLS) are examined quite thoroughly by the issue I linked.
Is this actually RLS aliasing? I think itâs a more basic consequence of left-to-right execution order.
I read this:
As âcopy the contents of address b to address a, then copy the contents of address a to address bâ.
It certainly rhymes with some of the aliasing problems with RLS, but I think this is just a case of Zigâs sometimes-surprising execution order. In other words, this is an intended result which probably wonât change.
A less surprising way to illustrate this:
a.*, b.* = .{ 42, a.* };
These are both going to be 42, right?
This touches on an earlier conversation about how Zigâs execution semantics can be surprising, but are the right choice for the kind of language it is.
From a language specification consistency and understandability standpoint, I definitely think the execution ordering is much better than a special-casing rule to perform the swapping âall at onceâ.
Semi-related topic on importance of order in struct initialization: