How can I map C++ specializations to Rust? What's the correct way to do it?

I come from a C++ background, and I'm trying to translate one of the programs I wrote back in the days to Rust, primarily for learning purposes.

The problem is this: I need to read some binary data through some library, and I have two cases:

  1. Data is a string
  2. Data is anything else (any kind of integer)

In C++, I'd simply specialize a template, which will only give compile-errors if something was not doable/reachable at compile-time; e.g., if I call a function on something that doesn't exist after everything is resolved.

I'm trying to achieve the same in Rust, here's my minimum reproducible example code. It can be found on playground here.

fn main() {
    let _x: TheStruct<String> = TheStruct::new();
}

#[warn(dead_code)]
struct TheStruct<T> {
    val: T,
}

impl<T> TheStruct<T> {
    // so this is a function that is supposed to call two possible implementations if T is a string or T is something else
    fn new() -> Self {
        // This call is supposed to chooses the impl below based on type
        TheStruct::<T>::make_one()
    }
}

// this is String impl
impl TheStruct<String> {
    fn make_one() -> Self {
        TheStruct::<String> {
            val: String::new(),
        }
    }
}

// this is "everything else" impl
impl<T: Default> TheStruct<T> {
    fn make_one()-> Self {
        TheStruct {
            val: T::default()
        }
    }
}

How can I achieve this in Rust? I'd appreciate helping me to get this to compile.

Also I'd like to point out that some implementations should be the same for both String and other types. I mean: I don't want to specialize everything, i.e., all methods, for String.

You could use specialized impl blocks, or an enum if you wanted that:

struct Foo<T> (T);

impl<T> Foo<T> {
    //Non specialized implementations
}

impl<T: Debug> Foo<T> {
    //Implementations where T is debug
}

impl Foo<String> {
    //Implementation for string
}

If you want to limit the generic type, you could use a trait or an enum.

To clarify, using an enum would look like this:

enum Foo {
    Str(String),
    U8(u8), I8(i8),
    U16(u16), I16(i16),
    U32(u32), I32(i32), F32(f32)
    U64(u64), I64(i64), F64(f64),
    U128(u128), I128(i128),
    Usize(usize), Isize(isize),
}

Which might look ugly (Especially with so many variants), so here's a trait version:

enum Foo<T: Number> {
    Str(String),
    Num(T),
}

trait Number {}
macro_rules! impl_number {
    ($($name:ident),+$(,)?) => {
        impl Number for $name {}
    }
}
impl_number!(
    u8, i8, u16, i16,
    u32, i32, u64, i64,
    f32, f64, u128, i128,
    usize, isize, ! //! is in the case there is no number variant
);

Or, with a struct and avoiding the unstable ! never type:

impl_number!(
    ...
   String,
);
struct Foo<T: Number>(T);

But then we cannot get any specialization, so the enum is probably the clearest way to go for specialization, at least in my opinion.

1 Like

C++-style specialization isn't available in Rust today (though it's in development). To solve your specific problem without using specialization, consider that "everything else" and "any integer" aren't the same thing -- the universe of values that aren't strings includes vectors, URLs, file handles.... C++ SFINAE lets you ignore these types; Rust does not.

So I would likely implement the operation for the integer types I care about, and String, and ignore the rest. (Or maybe I want to also have special behavior for URLs -- we could make that happen.)

Also, the operation you're implementing polymorphically should be defined on a trait, like so:

trait MakeOne {
  fn make_one() -> Self;
}

impl MakeOne for TheStruct<String> {
  fn make_one() -> Self { ... }
}

impl MakeOne for TheStruct<u32> {
  fn make_one() -> Self { ... }
}

Then, the functions that rely on this would need a trait bound:

impl<T: MakeOne> TheStruct<T> {
  fn new() -> Self {
    TheStruct::<T>::make_one()
  }
}
3 Likes

If you are willing to use nightly, then you can do something like this

3 Likes

Thanks to everyone for suggesting a solution and trying to help. The most promising solution I see is the one from @RustyYato. I'm OK with using nightly for now. But I seem to not fully understand how this works. If I try to instantiate another i64 version of my struct, it doesn't compile. Take a look: Rust Playground

I would actually consider using specialization for this to be a mistake and quite unidiomatic. It is a design error in C++ that it lets you get away with sloppiness like "anything that is not a string is an integer". When you consider a (closed) set of types to be similar for a given task, it's way clearer to do what @cbiffle suggested and go with a custom trait, explicitly implemented only and exactly for the types you care about.

5 Likes

Yes, you're right... but it's crazy to rewrite the same implementation for every integer! I consider this a trade-off between having to rewrite everything for having idiomatic perfection vs writing stuff once to make the code maintainable. Because honestly, rewriting the same implementation 8 times (at least) for every type is the worst mistake anyone can do. I'm a big fan of DRY coding.

Am I misunderstanding the alternative? Is there a way to write one implementation for all integers?

That's merely a sign of a level of abstraction missing from the standard library. In the meantime, you can use something like num_traits which contains the necessary generalization over all primitive integer types.

Or you can generate the trait impls yourself by using a macro, which helps you avoid manually typing out 12 or so of them.

2 Likes

Can you please provide a little example of that (num_traits), preferably in rust playground? Not sure how to use that in this context.

Sure.

1 Like

Ideally you'd identify an existing trait that corresponds to the set of types you want and allows you the operations you want (e.g. from num_traits). If that is not the case, the common technique is to use a simple probate macro_rules macro to avoid repeating the implementation.

Something is wrong with num_traits... my compiler is complaining that it's not found. I found it as a crate, but the playground code you provided works without any problem without importing a crate. Can you please explain how this works together?

error[E0432]: unresolved import `num_traits`
  --> src\main.rs:5:5
   |
5 | use num_traits::PrimInt;
   |     ^^^^^^^^^^ use of undeclared type or module `num_traits`

Are you familiar with how cargo and building Rust projects in general works? If you want to use a crate, you need to explicitly list it in your Cargo.toml file under dependencies. The playground adds such dependency declarations for several of the most used/important crates by default, that's why it "just works" there.

Oh, I see. Yes, I'm familiar with cargo. I was just surprised that it worked with no extern crate on playground.

extern crate is no longer necessary for Rust 2018 programs.

1 Like

Plus the Playground has a huge list of dependencies in its Cargo.toml.

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