Can I do C++'s SFINAE

I was wondering about this code:

#[inline(always)]
pub fn multiply_u128<T>(a: T, b: T, result: &mut[u64])  {
    let r = a as u128 * b as u128;
    //fill result with 64 bit chunks
}

it will work for integers except when T is u128. In this case we could get overflows.
Is it possible to make this function work with u8, u16, u32, u64 but not u128?

Implement a trait for u8, u16, u32, u64, and bound T by that trait.

2 Likes

To answer the thread's title, rather than the XY problem that lead to it (go for @skysch suggestion there), for other people stumbling upon this question:

Rust doesn't really have SFINAE, except in the method resolution and assoc item resolution algorithm, where it allows shadowing:

  • inherent assoc item can shadow a trait-provided one;

  • a .method() which does not require dot-auto{,de}ref can shadow one that does.

So, if you feature a shadowing thing which is only available based on implementing a trait / or some other trait guards —except for lifetime-based ones—, and then have a potentially-shadowed thing which is unguarded, you'll effectively have a form of SFINAE w.r.t. the trait bounds: if they're met, it uses the shadowing assoc item, else it uses the other one, which kind of acts as a fallback (this is the key aspect of SFINAE, as I view it, and this is the "common" aspect in Rust).

  • If using the .method() approach, this leads to:

    1. Autoref-based specialization, if using .-dot operator auto-ref;

    2. Autoderef-based specialization, a generalized variant of the previous ones (allows to easily feature multiple "SFINAE layers"), based off, you guessed it, .-dot operator auto-deref.

    The only drawback of these approaches is that they require trait-based methods, which currently cannot be const in stable Rust, which thus make it incompatible with constants, and, from there, unable to be used for type resolution.

  • Using the inherent vs. trait-provided assoc item (fn or const) approach, one can then thus feature "SFINAE" constants, and with them, using a const-generic type, one can eventually reach "SFINAE" types.

Note that any of these "SFINAE" approaches are:

  • very boilerplate-y,

  • unable to properly work with generic types (if you have a fallback for a !Sized case, say, then given <T : ?Sized>, the fallback path will be taken, for all Ts, even if later on the specific choice of T happened to be Sized. The same applies for the other traits.

Both of these things make these "Rust SFINAE hacks" be only truly suitable for macros, which can hide the boilerplate, and which can handle any caller-provided type (a form of (meta-)polymorphism), without involving generics.

Example

Here is an example of the inherent-assoc-based approach: we'd like to feature a wait! macro that shall be given a Future —or not!— and it shall .await it if it is one, or do nothing if it isn't.

  • In practice, this is implemented as a conversion to a future that is unconditionally .awaited, where that future is value if it is a future already, or async { value } otherwise.
    The semantics could then be viewed as async { value }.flatten().await, where the .flatten() is the true SFINAE-based implementation.
#[doc(hidden)] /** Not part of the public API */ pub
mod __ {
    // To get an inherent method for _any_ type we use a wrapper type.
    pub struct Wrapper<T>(pub T);
    
    // SFINAE attempt: inherent assoc items
    impl<T> Wrapper<T>
    where
        T : ::core::future::Future, // the "fallible guard"
    {
        pub
        fn __into_fut_flattened (self: Self)
          -> T // impl Future<Output = T::Output>
        {
            self.0
        }
    }
    
    #[extension(pub trait Fallback)] // From `::ext-trait`
    impl<T> Wrapper<T> {
        fn __into_fut_flattened (self: Self)
          -> ::futures::future::Ready<T> // impl Future<Output = T>
        {
            ::futures::future::ready(self.0)
        }
    }
}

and with it, the macro:

#[macro_export]
macro_rules! wait {( $e:expr $(,)? ) => (
    match $crate::__::Wrapper($e) { wrapper => {
        #[allow(unused_imports)]
        use $crate::__::Fallback as _;
        
        wrapper.__into_fut_flattened()
    }}.await
)}

Demo

#[::tokio::main]
async
fn main ()
{
    assert_eq!(42, wait!(42));
    assert_eq!(42, wait!(async { 42 }));
}
  • Playground

  • (With this, a proc-macro could try to wait!()-wrap all the function calls inside an async fn function body, thus allowing for the magical "implicit .await" magic / proposals).

2 Likes

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.