I've ended up creating some code that kinda looks like this:
use std::any::{TypeId, type_name};
use std::mem::transmute;
struct Thing<T>(T);
#[derive(Default)]
struct Helper<T>(T);
impl Helper<i32> {
fn i32(&self) {
println!("i32!");
}
}
impl Helper<u32> {
fn u32(&self) {
println!("u32!");
}
}
enum KnownThings {
I32(Thing<i32>),
U32(Thing<u32>)
}
fn do_thing<T: 'static>(thing: KnownThings, helper: &Helper<T>) {
match thing {
KnownThings::I32(i) => {
assert!(TypeId::of::<T>() == TypeId::of::<i32>());
let helper: &Helper<i32> = unsafe { std::mem::transmute(helper) };
helper.i32();
}
KnownThings::U32(i) => {
assert!(TypeId::of::<T>() == TypeId::of::<u32>());
let helper: &Helper<u32> = unsafe { std::mem::transmute(helper) };
helper.u32();
}
}
}
fn main() {
do_thing(KnownThings::I32(Thing(1)), &Helper::<i32>::default());
do_thing(KnownThings::U32(Thing(3)), &Helper::<u32>::default());
}
I was actually a bit surprised this even worked. Is transmuting Helper like this safe? Is there a better way I should be going about this?
And here's how your code would look like with downcast_ref
instead of your unsafe
transmute.
use std::any::Any;
struct Thing<T>(T);
#[derive(Default)]
struct Helper<T>(T);
impl Helper<i32> {
fn i32(&self) {
println!("i32!");
}
}
impl Helper<u32> {
fn u32(&self) {
println!("u32!");
}
}
enum KnownThings {
I32(Thing<i32>),
U32(Thing<u32>)
}
fn do_thing<T: 'static>(thing: KnownThings, helper: &Helper<T>) {
match thing {
KnownThings::I32(_) => {
let helper: &Helper<i32> = (helper as &dyn Any).downcast_ref().unwrap();
helper.i32();
}
KnownThings::U32(_) => {
let helper: &Helper<u32> = (helper as &dyn Any).downcast_ref().unwrap();
helper.u32();
}
}
}
fn main() {
do_thing(KnownThings::I32(Thing(1)), &Helper::<i32>::default());
do_thing(KnownThings::U32(Thing(3)), &Helper::<u32>::default());
}
4 Likes
Thanks for this thread (and the example!). I particularly like the trait dispatch method from that thread, but in my real code I want to call a function on Thing that takes a specific Helper, ie:
fn use_helper<T>(thing: &Thing<T>, helper: &Helper<T>)
Would you know if it would be possible to call this from do_thing
this via trait dispatch as well, or is the Any
solution my best option?
Is something like this what you’re looking for?
struct Thing<T>(T);
#[derive(Default)]
struct Helper<T>(T);
enum KnownThings {
I32(Thing<i32>),
U32(Thing<u32>),
}
#[derive(Copy, Clone, Debug)]
struct WrongThing;
impl TryInto<Thing<i32>> for KnownThings {
type Error = WrongThing;
fn try_into(self) -> Result<Thing<i32>, WrongThing> {
if let Self::I32(x) = self {
Ok(x)
} else {
Err(WrongThing)
}
}
}
impl TryInto<Thing<u32>> for KnownThings {
type Error = WrongThing;
fn try_into(self) -> Result<Thing<u32>, WrongThing> {
if let Self::U32(x) = self {
Ok(x)
} else {
Err(WrongThing)
}
}
}
fn use_helper<T>(thing: &Thing<T>, helper: &Helper<T>) {
dbg!(std::any::type_name::<T>());
}
fn do_thing<T>(thing: KnownThings, helper: &Helper<T>)
where
KnownThings: TryInto<Thing<T>, Error: std::fmt::Debug>,
{
use_helper(&thing.try_into().unwrap(), helper);
}
fn main() {
do_thing(KnownThings::I32(Thing(1)), &Helper::<i32>::default());
do_thing(KnownThings::U32(Thing(3)), &Helper::<u32>::default());
}
3 Likes
Yes! This is exactly what I was looking for. I always find it hard to tackle such generic code. Thanks!
the code (I assume you reduced from more complex code) in your example is a bit misleading. the transmute()
in this particular example is safe, only because of the TypeId
assertions. for example, the type signature does NOT prevent this from compiling, but the result is a panic because of the assertion failure. if the type assertions are not there, you'll get UB:
do_things(KnowThings::I32(Thing(1)), &Helper::<i64>::default());
the key to understand this code is the concept of monomophization, that is, when you call the generic function do_things<T>(...)
with different type `T, you are NOT actually calling the same function, but different "instances" of it.
in fact, to understand how monomophization works and why your example worked, the KnowThings
and Helper
are distractions, do_things()
can be further reduced to something like this,
fn do_things<T: 'static>(x: &T) {
if TypeId::of::<T>() == TypeId::of::<i32>() {
let x: &i32 = unsafe { transmute(x) };
println!("i32: {x}");
} else if TypeId::of::<T>() == TypeId::of::<u32>() {
let x: &u32 = unsafe { transmute(x) };
println!("u32: {x}");
}
}
the key to understand this code is, with each instance of T
, only one branch is executed, so the transmute()
is actually between the same type .