1

In Rust I would like to give a name to a class, and this class owns this name.

Sometimes the name is passed by an String. For this case I can just simply move the ownership.

But sometimes this name is given by a static string(&str). For this case I want to refer to that string, rather than making a String from it.

My question is: how can I declare this name field in my class? What type should it be?


Some updates/background on the requirements:

  • The name will be immutable.
  • The reason why I want to distinguish a String and a &str is that I want to reduce the dynamic allocation to the minimum.
1
  • 3
    Maybe Cow? Feel free to write an answer about this, if it helps... Commented Nov 21, 2016 at 21:22

2 Answers 2

4

One option is to declare the name member as an enum that can either contain a String or an &'static str:

enum Name {
    Static(&'static str),
    Owned(String),
}

struct Class {
    name: Name,
    // ...
}

The class can then provide appropriate constructors (there will have to be two) and a get_name() method for accessing the name as string slice:

impl Class {
    pub fn new_from_str(name: &'static str) -> Class {
        Class { name: Name::Static(name) }
    }
    pub fn new_from_owned(name: String) -> Class {
        Class { name: Name::Owned(name) }
    }

    pub fn get_name(&self) -> &str {
        match self.name {
            Name::Owned(ref s) => s.as_str(),
            Name::Static(s) => s,
        }
    }
}

fn main() {
    let c1 = Class::new_from_str("foo");
    let c2 = Class::new_from_owned("foo".to_string());
    println!("{} {}", c1.get_name(), c2.get_name());
}

The other option is to use the Cow type provided by the standard library for this purpose:

use std::borrow::Cow;

struct Class {
    name: Cow<'static, str>,
}

Since Cow implements the Into trait, the constructor can now be written as a single generic function:

pub fn new<T>(name: T) -> Class
    where T: Into<Cow<'static, str>> {
    Class { name: name.into() }
}

Cow also implements the Deref trait, allowing get_name() to be written as:

pub fn get_name(&self) -> &str {
    return &self.name;
}

In both cases the name member will equal the size of the larger variant plus the space taken by the discriminator. As String is the larger type here, and it takes up three pointer sizes (the string contents is allocated separately and doesn't count), Name will take four pointer sizes in total. In case of explicit enum, the member can be made smaller still by boxing the string:

enum Name {
    Static(&'static str),
    Owned(Box<String>),
}

This will cut down the size of Name to three pointer sizes, of which one slot is used for the discriminator and the remaining two for the string slice. The downside is that it requires an additional allocation and indirection for the owned-string case - but it might still pay off if the majority of your class names come from static string slices.

Sign up to request clarification or add additional context in comments.

7 Comments

Could you also give more information on the size of enum Name?
@TheBusyTypist As always in Rust, the size of an enum equals the size of its largest member, plus the size of the discriminator. In this case, the larger member would be String, which amounts to three pointer-sized machine words. (Here size refers to the size of the struct, which does not include the contents.) Due to alignment, the whole Name enum amounts to the size of four pointers.
@TheBusyTypist I've updated the answer to include the option of boxing the name, which cuts down the size to three pointers. Given that &str is a slice represented with pointer and usize, I don't think you can make it smaller than that, except using unsafe trickery with pointer masking.
That's a great example of using Cow. I was just about to edit my code but saw you used Into as well :)
@squiguy Yeah, I've just about reinvented Cow. I would have deleted the enum code, except it might be interesting for learning about enums (which Cow uses internally anyway) and, more importantly, it allows the additional boxing size optimization.
|
0

What you can do to allow for a mix of types is use the Into trait. This is versatile in that it makes sure a safe conversion happens between the types.

A str slice can be converted "into" an owned String as such.

Some test code to demonstrate it:

#[derive(Debug)]
struct Test {
    name: String,
}

impl Test {
    pub fn new<T: Into<String>>(t: T) -> Test {
        Test { name: t.into() }
    }
}

fn main() {
    let t = Test::new("a");
    println!("{:?}", t);
}

2 Comments

Thank you for your reply. But I thought we still dynamically allocate a copy of "a" for name, which I think is not necessary. Is it possible to eliminate this copy?
Ah, I didn't know that this was a requirement. It sounds like Cow is your best bet after all.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.