Difference between type age = u8 and struct age(u8)?

Greetings!
I have seen this thread already, but it doesn't address my actual question.

I plan on nesting more descriptive single variable types in actual structs. So in this example, I want to refer to my own custom type "age" instead of u8, so I have one place where I may want to upgrade it later to a different Rust type. I don't need to apply traits to this type.

Also, intuitively, it seems easier to read in code

type age = u8; 

than

struct age(u8);

And in the bigger picture, I would imagine that performance is better on a type alias than struct, no? Does it matter? Other than not having traits on the atomic type alias, are there any other consequences for nesting a type alias inside a bigger struct, versus nesting pure structs?

May libraries/frameworks throw a fit when a struct is comprised of type aliases rather than nested structs, vice versa?

Thanks!
-Bernd

EDIT: of course I meant struct age(u8) in the original question (parentheses instead of curly braces) :slight_smile:

The main practical difference is that structs are different types, whereas type aliases are just additional names for the same types. So if you do this:

type Age = u8;
type Weight = u8;

fn calculate(age: Age, weight: Weight) {
    todo!()
}

There is no type checking for which argument is which.

let age = 5;
let weight = 60;
// these both compile
calculate(age, weight);
calculate(weight, age);

Structs are different types, so you can't confuse one for the other.

struct Age(u8);
struct Weight(u8);

fn calculate(age: Age, weight: Weight) {
    todo!()
}

let age = Age(5);
let weight = Weight(60);
calculate(age, weight);
// this fails to compile
calculate(weight, age);

There is zero performance difference. Rust structs imply no indirection, so Age(u8) and u8 look the same in memory. There is also no type information being stored at runtime.

This shouldn't be an issue at all, but I'm not sure what kind of problems you'd expect to happen here. Is this an issue in another language?

10 Likes

In addition to there being limits on what traits you can implement for foreign types, you can't write inherent methods for foreign types. But you could create a local trait and implement that for the foreign type instead, to overcome this.

It's a breaking change to redefine a type alias, whereas with a newtype (a struct wrapping a single field of some other type), you're more free to change the types of private fields.[1] This may not matter if your type alias is private or you're writing a leaf crate.

If you use a type alias and unwittingly rely on some properties of the type you didn't mean to, like certain trait implementations, changing the type alias can create more work in your own crate than you anticipated. Whereas a newtype only has the implementations you provide. On the other hand, you may find you need to provide a lot of implementations for a newtype upfront.

Eh, one would hope not, but it's possible some optimizations won't see through a newtype. Such as specialization utilized in std. (If you want to be sure your newtype has the same layout, put #[repr(transparent)] on it; otherwise its not guaranteed.)

On the flip side, if you have a newtype with more invariants than the inner type, you can sometimes make more performant implementations that rely on those invariants yourself.

I wouldn't worry either way unless you have measurements showing that it's an actual concern.

You mean, when the fields of your local struct are aliases to foreign types instead of local newtypes around foreign types? They generally won't care. It may effect your ability to use derive macros or the like, if they rely on your fields implementing some trait (since you can't implement/derive foreign traits on foreign types, approximately speaking). But I feel that would be pretty rare/niche.


  1. But it can still be a breaking change if the new field type changes the structs variance, auto-traits, or prevents trivial drops. ↩︎

3 Likes

This is a key insight for me in helping distinguish the two. I can see how, if I were to change the u8 of the type alias approach to another Rust type, that everything downstream dependencies would break. Whereas struct age(u8) vs. struct age(u32) would still remain a type of "age", right?

Right.

I feel I had a run-in with Bevy when I used the type alias approach to components, but I don't recall the details right now (high chance, that I don't know my way around Bevy enough yet).
And the other consideration was....why would there be another mechanism (type alias) if the same can be done with a struct? Are there cases where the struct with single var tuple (newtype) will not work, but a type alias will. What can a type alias do, the newtype pattern doesn't? There must be a reason, and inquiring minds want to know :wink:

Very informative, thank you for that!

I would recommend reading about structural and nominal type systems. Rust (and for example C or Haskell) have nominal type system, and for example TypeScript of Go have structural type systems. If you only used structural typing languages before, then you may find nominal languages strange and vice versa. If you understand conceptual difference between them, you will also understand why struct Age(u8) would be considered idiomatic in one language, and not in the other.

2 Likes

Type aliases are mainly useful for convenience. Three possible cases I can think of:

  • Shortening complex types to some simple mnemonic. They should be transparently used as-is, but writing the whole type can be cumbersome (if it has several layers of generics, for example).
  • Providing a shortcut for some common cases. A good example is parking_lot::Mutex, which essentially simply provides the generic parameter to the underlying type.
  • Providing uniform interface for configuration-dependent types. A classic example is libc::c_int, which is currently always i32, but could be u32 or i64 on some platforms.
1 Like

That really surprised me when I discovered it. To my mind your example should not compile when accidentally swapping age and weight.

I don't know if I found this surprising because it worked differently in some other language I have used along the way.

But here we are defining a thing called age and a thing called weight and it turns out they are the same thing.

It's as surprising as would be to find out that struct Age(u8) and struct Weight(u8) were the same thing. They are the same size and shape after all.

Perhaps the keyword should have been alias or define ?

Perhaps. Sometimes I do accidentally write

type ShouldHaveUsedStruct {
    field: String,
    // ...
}

because hey, I am defining a type.

On the other hand, type will be more than just an alias mechanism when we get impl Trait in type (TAIT).[1]

Maybe alias would still have been less confusing though. :person_shrugging:


  1. Technically it's just barely more than an alias mechanism today already, but that's such a niche area of the language almost no one notices or knows about it ↩︎

1 Like

There is also another use I know about: providing something like type functions. For example, look at diesel type aliases like e.g. Find: it is basically a query to type system which produces some concrete type based on two other types and a trait which is implemented for them. Result is not necessary more complex, though for diesel DSLs it most of time is.

Unlike TAIT however you can still inline these type functions if you feel like it and code will work. With TAIT if you replace every occurrence of type alias with code after = you have high chances of making your code stop working as TAIT adds extra guarantees.


As for “zero performance difference” for struct A(u8) vs type A = u8 I believe that language promises this only if there is also #[repr(transparent)] before struct: I do not see any guarantees that compiler will not make changes that affect performance here and you might be able to add unnecessary padding to all such newtypes for testing in the future with flag similar to -Zrandomize-layout (does not look like adding padding is implemented in this or any other flag, but it was discussed in the linked feature request).

1 Like

If used in the particular case of foreign function interfaces, while the layout of the

#[repr(C)] 
struct foo(u8);

should be the same as a native u8, I believe there are architectures which can pass structs differently than native types. In this case, like @ZyX-II mentioned, #[repr(transparent) would be needed, as mentioned in the nomicon

Also, passing the struct/enum through FFI where the inner field type is expected on the other side is guaranteed to work. In particular, this is necessary for struct Foo(f32) or enum Foo { Bar(f32) } to always have the same ABI as f32 . For instance, there was one older example on targeting Emscripten.

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.