Specialization in Rust and Why It Is Still Unstable
Specialization would let a blanket impl have faster special cases. After years of work it is still nightly only, held back by soundness problems with lifetimes.

Specialization would let you write a general implementation of a trait for all types, and then a faster or more precise one for a few specific types, with the compiler choosing the most specific impl. The standard library already wants this: copying a slice of plain bytes could be a memcpy, and copying a slice of strings must clone each element. RFC 1210 proposed it in 2015, a nightly specialization feature has existed since 2016, and it is still not stable. This post shows what the feature looks like on nightly, the unsoundness that keeps it unstable, the narrower min_specialization the standard library uses internally, and the stable techniques you can use today to get the same effect. Everything here compiled on Rust 1.99 stable, or nightly where marked.
The problem it solves
Rust forbids overlapping trait implementations. A blanket impl and a more specific impl of the same trait overlap, because u8 matches both, and the compiler reports a conflict (E0119):
trait Describe {
fn describe(&self) -> String;
}
impl<T> Describe for T {
fn describe(&self) -> String {
"something".into()
}
}
impl Describe for u8 {
fn describe(&self) -> String {
"a byte".into()
}
}
fn main() {
println!("{}", 1u8.describe());
}error[E0119]: conflicting implementations of trait `Describe` for type `u8`
--> src/main.rs:11:1
|
5 | impl<T> Describe for T {
| ---------------------- first implementation here
...
11 | impl Describe for u8 {
| ^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `u8`The rule exists for good reason: with overlap, the choice of impl could depend on which crates are linked, and two crates could each add an impl that is valid alone and ambiguous together. Coherence (the guarantee that exactly one impl applies to any type) is what makes traits predictable, and the conflict rules are detailed in the orphan rule. Specialization asks for an exception: overlap is allowed when one impl is strictly more specific, and the less specific one marks its items as overridable with default.
What it looks like on nightly
#![feature(specialization)]
#![allow(incomplete_features)]
use std::fmt::Debug;
trait Describe {
fn describe(&self) -> String;
}
impl<T> Describe for T {
default fn describe(&self) -> String {
"something".into()
}
}
impl Describe for u8 {
fn describe(&self) -> String {
"a byte".into()
}
}
impl<T: Debug> Describe for Vec<T> {
fn describe(&self) -> String {
format!("a vec of {} items: {:?}", self.len(), self)
}
}
fn main() {
println!("{}", 7u8.describe());
println!("{}", 7u32.describe());
println!("{}", vec!["a", "b"].describe());
}a byte
something
a vec of 2 items: ["a", "b"]This ran on a nightly toolchain. The marker default on the blanket impl's method says "a more specific impl may replace this", and the compiler picks the narrowest applicable impl for each call. The feature name is flagged "incomplete" for a reason, and the compiler said so; the next section is why.
The soundness hole: lifetimes
Type-based specialization is straightforward. The unsolved problem is that lifetimes are erased after type checking, so by the time code is generated the compiler cannot tell &'a str from &'static str. An impl specialised for &'static str would be selected on a fact that the code generator no longer has. Generic code makes it worse: a function over T that calls a specialised method is compiled once per concrete type, and by then the lifetime is gone, so the choice of impl can depend on a lifetime relation that nothing checked. If the specialised impl relies on that relation to be memory-safe (say, by handing out a reference it believes is 'static), safe code can produce a dangling reference.
That is not a hypothetical. The tracking issue (rust-lang/rust#31844) holds minimised programs that use #![feature(specialization)] to turn a short-lived reference into a 'static one in safe code, which is why the compiler prints the "incomplete feature" warning when you enable it. It is the reason the feature has stayed on nightly for a decade: fixing it needs either a way to carry lifetime information into code generation, or a language rule that forbids specialising on anything lifetime-dependent. The second route is what min_specialization takes.
min_specialization: the safe subset the compiler itself uses
A restricted version, min_specialization, bans the dangerous patterns by allowing specialization only through a marker trait (#[rustc_specialization_trait]) that the compiler knows cannot depend on lifetimes, and by rejecting impls that mention a lifetime or repeat a type parameter. The standard library uses it for a handful of internal optimisations, such as the Vec::from_iter fast path that reuses the allocation of an owned iterator, and the specialised Clone for Copy slice contents. Users outside the standard library cannot name that marker trait on stable, so for ordinary code the feature remains out of reach even in its safe form.
What you can do on stable today
Five techniques give most of the benefit. In rough order of preference:
| Technique | Works when | Cost |
|---|---|---|
| Separate methods or traits | Caller knows the type (fill_bytes vs fill) | Two names to learn |
| Associated constant on the trait | Behaviour differs by a property (const IS_COPY: bool) | Each impl declares the property |
TypeId / Any check | Types are 'static and you accept a runtime branch | One compare, usually folded by the optimiser |
| An enum of cases | The set is closed | Match arms |
| Marker traits implemented by hand | You own the types | Boilerplate impls |
The associated-constant version is the closest stable equivalent to the textbook example. The trait carries a const, the generic code branches on it, and the compiler removes the dead branch at monomorphization time:
trait Packed {
const PLAIN_BYTES: bool;
fn encode(&self, out: &mut Vec<u8>);
}
impl Packed for u32 {
const PLAIN_BYTES: bool = true;
fn encode(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.to_le_bytes());
}
}
impl Packed for String {
const PLAIN_BYTES: bool = false;
fn encode(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&(self.len() as u32).to_le_bytes());
out.extend_from_slice(self.as_bytes());
}
}
fn encode_all<T: Packed>(items: &[T]) -> Vec<u8> {
let mut out = Vec::new();
if T::PLAIN_BYTES {
out.reserve(items.len() * 4);
}
for i in items {
i.encode(&mut out);
}
out
}
fn main() {
println!("{:?}", encode_all(&[1u32, 2, 3]));
println!("{:?}", encode_all(&["hi".to_string(), "rust".to_string()]));
}[1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0]
[2, 0, 0, 0, 104, 105, 4, 0, 0, 0, 114, 117, 115, 116]Each instantiation sees a constant in the if, so the unused branch costs nothing. The weakness is that the generic code has to know about the distinction, and a third-party type that does not set the const gets the default behaviour only if you gave the const a default. For types you do not control, add a second method on an extension trait instead, as in extension traits.
The TypeId trick, and its edges
When a generic function must act differently for one specific type and there is no trait you can change, a TypeId comparison does it at the cost of a 'static bound:
use std::any::{Any, TypeId};
fn describe<T: Any>(value: &T) -> String {
if TypeId::of::<T>() == TypeId::of::<u8>() {
let byte = (value as &dyn Any).downcast_ref::<u8>().unwrap();
format!("a byte ({byte:#04x})")
} else if let Some(s) = (value as &dyn Any).downcast_ref::<String>() {
format!("a string of {} chars", s.chars().count())
} else {
"something".to_string()
}
}
fn main() {
println!("{}", describe(&200u8));
println!("{}", describe(&String::from("héllo")));
println!("{}", describe(&3.5f32));
}a byte (0xc8)
a string of 5 chars
somethingThe comparison is on TypeId values known at compile time per instantiation, so the optimiser usually collapses it to the matching branch. What you give up is open extension: the list of special cases lives inside the function, and borrowed types cannot take part because Any demands 'static. It is the pragmatic pattern in serialisers, where one function treats u8 slices as bytes and every other slice as a sequence.
Should you wait for it?
No. The design problem is old and hard, and every year of waiting is another year of writing the const-and-branch version, which works and is explicit. A library that needs to behave differently for a type should say so in a trait, because explicit methods are easier to document and to test than behaviour that changes with the inferred type. If you are tempted by nightly specialization in an application, remember that the compiler rejects #![feature] on stable and the feature can change or vanish between nightlies; the lang team's current direction is covered in the tracking issue and the stable alternatives for the common cases (like the slice-of-Copy one) have already been implemented inside the standard library. For the sibling gap that people hit alongside this one, see trait aliases.


