Advanced Traits

Trait Specialization Limitations

Trait specialization is the ability to define a general blanket or default impl of a trait and override it with a more specific impl for a subset of types (for example, implementing CloneToVec via element-by-element .clone() for T: Clone, and overriding it with a single ptr::copy_nonoverlapping for T: Copy). In stable Rust, user-facing trait specialization (default fn) is not stabilized, and overlapping implementations are rejected with E0119.

Why General Trait Specialization Is Unsound Around Lifetimes

The primary reason full specialization (#![feature(specialization)]) remains unstable is lifetime erasure before monomorphization:

  1. Type checking and trait resolution happen before lifetimes are erased.
  2. Code generation (monomorphization) happens after lifetimes are erased—meaning rustc cannot tell at codegen time whether two lifetimes 'a and 'b inside a generic function fn foo<T>(x: T) were actually equal ('static) for a particular call!
  3. If a specialized impl only applies when T: 'static or when two references share the same lifetime (&'a u8, &'a u8), generic code and concrete code can select different implementations for the exact same value, breaking type soundness.
Why Lifetime-Dependent Specialization Breaks Soundness:

  Trait Impl 1 (General):     `impl<T>Behavior for T`
  Trait Impl 2 (Specialized): `impl<T: 'static> Behavior for T`

  Caller passes `&'static str` into generic `fn wrapper<U>(val: U)`:
    - Inside `wrapper<U>`, the compiler does NOT know `U: 'static`!
    - After lifetime erasure, `wrapper` selects Impl 1 while direct calls select Impl 2!

Safe Stable Idioms That Achieve Specialization Behavior

Even without language-level default fn specialization, stable Rust offers three zero-cost patterns for specialized behavior:

1. Adding Specialized Default Methods to the Trait Definition

The standard library uses this pattern extensively in Iterator: Iterator::nth, Iterator::count, and Iterator::last have general $O(n)$ default bodies in trait Iterator, while Vec::IntoIter and slice iterators override those specific methods in their own non-overlapping impl Iterator for std::slice::Iter<'a, T> blocks with $O(1)$ pointer arithmetic!

trait ByteBuffer {
    fn as_slice(&self) -> &[u8];

    // General default implementation ($O(n)$ scan):
    fn count_zeros(&self) -> usize {
        self.as_slice().iter().filter(|&&b| b == 0).count()
    }
}

struct RawSlice<'a>(&'a [u8]);
impl ByteBuffer for RawSlice<'_> {
    fn as_slice(&self) -> &[u8] {
        self.0
    }
}

struct ZeroFilledBuffer {
    len: usize,
}
impl ByteBuffer for ZeroFilledBuffer {
    fn as_slice(&self) -> &[u8] {
        &[]
    }

    // Specialized $O(1)$ override on a concrete type (no overlapping impl needed!):
    fn count_zeros(&self) -> usize {
        self.len
    }
}

fn main() {
    let raw = RawSlice(&[0, 1, 0, 2, 0]);
    let zeros = ZeroFilledBuffer { len: 1_000_000 };

    println!("raw zeros:   {}", raw.count_zeros());
    println!("zeros fast:  {}", zeros.count_zeros());
}
raw zeros:   3
zeros fast:  1000000

2. Autoref-Based Specialization in Macros (&&Wrapper vs. &Wrapper)

When writing declarative macros (like anyhow! or custom logging macros) that know the concrete type at the macro expansion site, method resolution searches inherent/trait methods on T before auto-referencing to &T and &&T. By implementing a higher-priority trait for &Wrapper<T> where T: Display and a fallback trait for Wrapper<T> where T: Debug, method-call auto-referencing selects Display when available and falls back to Debug when not:

use std::fmt::{Debug, Display};

struct Probe<T>(T);

trait ViaDisplay {
    fn render(&self) -> String;
}
impl<T: Display> ViaDisplay for &Probe<T> {
    fn render(&self) -> String {
        format!("Display: {}", self.0)
    }
}

trait ViaDebug {
    fn render(&self) -> String;
}
impl<T: Debug> ViaDebug for Probe<T> {
    fn render(&self) -> String {
        format!("Debug fallback: {:?}", self.0)
    }
}

#[derive(Debug)]
struct OnlyDebug(i32);

fn main() {
    // `(&Probe("hello")).render()` matches `ViaDisplay` on `&Probe<&str>` first!
    println!("{}", (&Probe("hello")).render());
    // `OnlyDebug` does not implement `Display`, so method resolution derefs `&Probe`
    // to `Probe<OnlyDebug>` and calls `ViaDebug::render`!
    println!("{}", (&Probe(OnlyDebug(42))).render());
}
Display: hello
Debug fallback: OnlyDebug(42)