#[repr(transparent)] – Why?

From the reference on type layout:

The Default Representation

Nominal types without a repr attribute have the default representation. Informally, this representation is also called the rust representation.

There are no guarantees of data layout made by this representation.

That explains why I need #[repr(transparent)] when I want to do certain things (e.g. expect an equal layout for passing values to an FFI or transmuting values).

But what I don't understand: Why aren't structs that can be made transparent (i.e. structs with a single non-zero-sized member) always transparent by default? Would there be any downside? Does that have something to do with trait objects and Debug?


RFC 1758 says:

On some ABIs, structures with one field aren't handled the same way as values of the same type as the single field. For example on ARM64, functions returning a structure with a single f64 field return nothing and take a pointer to be filled with the return value, whereas functions returning a f64 return the floating-point number directly.

I wonder why is that, i.e. what's the reason for the ABI to be non-transparent. Is that due to some standard outside the Rust world?

5 Likes

Why aren't structs that can be made transparent always transparent by default?

Making a type transparent is an API commitment. You're making guarantees to the users of this type so if you ever make changes then that will be a "breaking change". In Rust such commitments are opt-in (same goes for traits such as Debug, Clone, etc taking them away is a breaking change).

And yes ABI can matter. More generally speaking, specifying a specific layout can prevent optimizations. The compiler is free to move things around so long as there's no "observable" effect. By making a type transparent you make its layout observable and thus (potentially) give the compiler less room for optimizations.

12 Likes

So if T has some constraints on layout, then Wrapper(T) shouldn't (by default), because it could inhibit optimations in the latter case. That makes sense now to me. Thanks.

C++ has made a mistake of defining its ABI in the way that prevents unique_ptr<T> from being passed around as efficiently as T*, even though it's essentially just a pointer. So C++ would benefit from a #[repr(transparent)] there!

However, AFIAK Rust was careful not to make the same mistake (values aren't promised to all have a unique stable address), so I don't see a technical reason why Rust couldn't promise #[repr(transparent)] for all single-field wrapper types (or more specifically, types with only one non-zero-sized field).

It probably exists only to be explicit in the APIs, and an insurance just in case Rust wanted to change layout of structs in the future.

1 Like
#[repr(C)]
struct Foo {
    a: usize,
    b: usize,
}

struct Bar(Foo);

#[repr(transparent)]
struct Baz(Foo);

would require Baz to be passed on the stack at least for some ABI's, while Bar will be passed in two registers by default for the "Rust" abi.

5 Likes

The critical difference is between what it happens to do now vs what it's guaranteed to do forever.

For example, I suspect that the compiler happens to usually treat newtypes as if they're transparent even without the attribute. But there's no guarantee of that. It could in future maybe more-align things, for example.

So if you just want it to be optimized, leave it repr(rust) and trust that the compiler will probably make a reasonable choice.

But if you depend on something -- for logical correctness, or more importantly for soundness -- the philosophy is to require that that be marked explicitly. That way the compiler can check that it will actually keep doing whatever you're relying upon, and also helps people new to the codebase know that it's happening, so they're less likely to violate it. (The compiler will error if they add another field that's not a 1-ZST to help keep from making that mistake, for example.)

For a more specific example, struct Foo(u8, u16, u8); used to be the same with or without repr(C). But when someone implemented struct rearranging, so now it's 2 bytes smaller in repr(Rust) than in repr(C). It's important anyone depending on it matching C put that marker to allow rust to change it for people who don't need the additional constraint, in order to make code faster for the majority.

6 Likes

I recommend reading these notes. Even with a newtype, unless you use #[repr(transparent)], you could get a different ABI for example.

(Edited to correct saying the exact wrong thing.)

2 Likes

I don't understand. It says:

#[repr(transparent)] on a struct with a single field gives it the ABI of its field.

1 Like

Ugh, sorry, I typed the exact opposite of what I meant I guess. Unless you #[repr(transparent)].

1 Like

Haha, okay, no problem.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.