Description
Proposal
Problem statement
To provide an API for comparing TypeId
s in const
contexts that can be stabilized "soon" along with TypeId::of
without depending on const
in trait impls. This API should make stabilization of TypeId
in const
contexts uncontentious, but isn't interpreted as a commitment to actually do that stabilization.
Motivating examples or use cases
TypeId
s can be used in const
contexts as a limited form of specialization; they can be used to dispatch at compile-time based on the type of a value. As a real-world example, I have a library that allows capturing values using a standard trait, like fmt::Display
, but specializes internally when that value is a primitive like a string or integer:
use std::{any::TypeId, fmt};
#[derive(Debug)]
pub enum Value<'a> {
I32(i32),
// Other variants for other primitives...
Debug(&'a dyn fmt::Debug)
}
impl<'a> Value<'a> {
pub fn capture_debug<T: fmt::Debug + 'static>(value: &'a T) -> Value<'a> {
value_from_primitive(value).unwrap_or(Value::Debug(value))
}
}
// NOTE: Can't currently be `const`; support was removed in https://github.com/rust-lang/rust/pull/103291
// `T` is unsized so `str` can be supported as well
const fn value_from_primitive<'a, T: ?Sized + 'static>(value: &'a T) -> Option<Value<'a>> {
let id = TypeId::of::<T>();
if id == TypeId::of::<i32>() {
// Using `TypeId` instead of `Any::downcast` because `T` is unsized; so we can't convert into a `dyn Any`
// without losing the `'static` bound
// SAFETY: `T` has been asserted to be `i32`
return Some(Value::I32(unsafe { *(value as *const T as *const i32) }))
}
// possibly many other branches...
None
}
fn main() {
// Prints `I32(42)`
println!("{:?}", Value::capture_debug(&42i32));
// Prints `Debug(42)`
println!("{:?}", Value::capture_debug(&42u32));
}
This pattern is useful when working with dynamic data such as when templating or wiring up loosely-coupled state. Unfortunately, it doesn't currently work even on nightly
because structural-matching of TypeId
s has been removed (for perfectly valid reasons). The only thing you can do with a TypeId
is compare it with other TypeId
s, so without some const
way to compare them, TypeId
is currently useless in const
contexts.
Solution sketch
This proposes TypeId::matches
; an API for asserting two TypeId
s match at compile-time that could be stabilized "soon" alongside TypeId::of
to make TypeId
s usable in const
functions:
impl TypeId {
/// Whether this type id is the same as `other`.
///
/// If a `TypeId` matches another it means they were both instantiated from
/// the same generic type `T`.
///
/// This method is equivalent to equality, but can be used at compile-time.
///
/// # Examples
///
/// ```
/// #![feature(const_type_id)]
///
/// use std::any::TypeId;
///
/// let typeof_string = TypeId::of::<String>();
/// let typeof_bool = TypeId::of::<bool>();
///
/// assert!(typeof_string.matches(typeof_string);
/// assert!(!typeof_bool.matches(typeof_string);
/// ```
#[unstable(feature = "const_type_id", issue = "77125")]
#[rustc_const_unstable(feature = "const_type_id", issue = "77125")]
pub const fn matches(&self, other: TypeId) -> bool {
self == other
}
}
It relies on the standard library having some kind of support for const
equality, without exposing what that support is. It decouples this pattern of specialization from const
trait support or from full specialization.
Our example from before becomes:
const fn value_from_primitive<'a, T: ?Sized + 'static>(value: &'a T) -> Option<Value<'a>> {
let id = TypeId::of::<T>();
if id.matches(TypeId::of::<i32>()) {
// Using `TypeId` instead of `Any::downcast` because `T` is unsized; so we can't convert into a `dyn Any`
// without losing the `'static` bound
// SAFETY: `T` has been asserted to be `i32`
return Some(Value::I32(unsafe { *(value as *const T as *const i32) }))
}
// possibly many other branches...
None
}
Alternatives
You could do this using plain-old ==
, but that needs const
trait support, which is still in its design stages. When equality does become possible in const
contexts, this method is simply a semantic alternative to it that's also a good place to document what the implications of two TypeId
s being equal or not are.
You could also use specialization, which isn't being actively pushed and has no clear path to stabilization.
You could use Any::is
and Any::downcast_ref
for this, but Any
requires Sized + 'static
(Sized
for coercing to dyn Any
and 'static
from its trait bounds), which rules out unsized types like str
.
Links and related work
What happens now?
This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.
Possible responses
The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):
- We think this problem seems worth solving, and the standard library might be the right place to solve it.
- We think that this probably doesn't belong in the standard library.
Second, if there's a concrete solution:
- We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
- We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.