Is using struct to wrap basic types a good idea?

Hi,

I’m currently manipulating different u32 with different meaning (user’s id, store’s id etc…)

Is it a good idea to do things like:

pub struct StoreId(u32);
impl Deref

In order to distinguish them and leverage the compiler to avoid passing a UserId instead of a StoreId and vice versa?

Well, if you want the StoreId and UserId to have different types, then yes, you would want to create their own tuple struct for each. About the Deref though, that seems kind of odd, as though the point of adding a type to interface with the actual u32 is to implement its own functions.

pub struct StoreId(pub u32);
pub struct UsersId(pub u32);

Is just fine, and then you can implement functions for these:

impl StoreId {}
impl UsersId {}
3 Likes

Yes. This has a specific name: the newtype pattern. It’s discussed in the book.

Completely agree. Newtypes should generally not implement Deref because they end up breaking the abstraction boundary you are trying to create.

See also:

6 Likes

Thanks all, I was using Deref to access the u32 but it’s not needed.

Is there any kind of perf trade-off with this newtypes pattern?

Usually this kind of newtype pattern will compile to a bare u32, so there shouldn’t be any kind of runtime performance drop.

2 Likes

But how to access to the functions available to u32 without reimpl them?
Like to_string stuff like that?

That’s why you define it like

struct UserId(pub u32);
//            ^^^

To allow access to the internal data if so required.

1 Like

I see, that’s why I was referring to Deref, to avoid having user_id.0 which look kinda unnatural to me

There’s no reason to use a tuple struct if you don’t like it:

struct UserId { as_int: u32 }
1 Like

Regarding Deref, I personally don’t completely agree with what has been said above: imho it actually depends on the kind of abstraction you are building:

  • if your main concern is abstraction from the implementation, then both Deref ought not to be implemented and the inner field(s) ought to be private (i.e., not pub);

  • but if your concern is about “type specialization”, i.e., “subtyping” in OOP terms (which seems to be your case), then not only do you declare the sole inner field pub, but you may also impl Deref (and even impl DerefMut) to get “inhericance-like” ergonomics. For instance, you could have struct UserName(pub String);, in which case having Deref[Mut] could come in handy.

6 Likes

It’s exactly what I’m doing, thank you!

I just want a type “alias” that the compiler can check (if I get it correctly, type creates true aliases that are considered identical)

1 Like

Yes, and you can use either a tuple struct or a struct with named members for that. They are functionally identical in Rust.

1 Like

Integers have many, many operations that make no sense for entity identifiers. For example, you’re almost certainly not supposed to subtract or multiply ids, count zeros or reverse byte order. Hence it makes sense to treat ids as black boxes, with the type of the underlying representation scarcely more than an implementation detail. If/when you do need to expose the representation, it is good that you have to write it out explicitly.

4 Likes

True, I would definitely define something along the lines of the following Id abstraction

#[derive(
    Debug,
    Default,
    Clone, Copy,
    PartialEq, Eq,
    Hash,
    PartialOrd, Ord,
    Serialize, Deserialize
)]
pub
struct Id (u32);

impl From<u32> for Id {
  // ...
}
impl From<Id> for u32 {
  // ...
}

impl Id {
    pub
    fn next (self: Self) -> Option<Self>
    {
        u32::checked_add(self.into(), 1).map(Self::from)
    }

    pub
    fn prev (self: Self) -> Option<Self>
    {
        u32::checked_sub(self.into(), 1).map(Self::from)
    }
}

pub
mod iter {
    use super::*;

    fn ids_from (start: Id) -> impl Iterator<Item = Id>
    {
        core::iter::successors(Some(start), |&prev| prev.next())
    }

But then, when wanting a type-level distinction between a UserId, ElemId, etc., I would still use “transparent newtype wrappers”:

#[derive(...)]
pub
struct UserId { pub id: Id }

#[derive(...)]
pub
struct ElemId { pub id: Id }

If you’re gonna newtype, might as well go all out with a macro for it.

#[macro_export]
macro_rules! newtype {
  ($(#[$attr:meta])* $new_name:ident, $v:vis $old_name:ty) => {
    $(#[$attr])*
    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
    #[repr(transparent)]
    pub struct $new_name($v $old_name);
    impl $new_name {
      /// A `const` "zero value" constructor
      pub const fn new() -> Self {
        $new_name(0)
      }
    }
  };
  ($(#[$attr:meta])* $new_name:ident, $v:vis $old_name:ty, no frills) => {
    $(#[$attr])*
    #[repr(transparent)]
    pub struct $new_name($v $old_name);
  };
}

The usages are things like:

newtype! {
/// Records a particular key press combination.
KeyInput, u16
}
newtype! {
/// You can’t derive most stuff above array size 32, so we add
/// the , no frills modifier to this one.
BigArray, [u8; 200], no frills
}

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.