Negative trait bounds using feature(specialization)

Theoretically, this code should work.
The library part compiles without any errors.
The usage part can indeed reject the types that it is supposed to reject.
However, the types that should be accepted are also being rejected.

#![feature(specialization)]

pub struct True;
pub enum False {}

pub trait CopyImpled {
    type Impled;
}

pub struct Indirect<T: ?Sized>(core::marker::PhantomData<T>);

impl<T: ?Sized> CopyImpled for Indirect<T> {
    default type Impled = False;
}

impl<T: Copy + ?Sized> CopyImpled for Indirect<T> {
    type Impled = True;
}
// lib end

//struct Test<T>(T) where T: !Copy;
struct Test1<T>(T) where Indirect<T>: CopyImpled<Impled = False>;

struct Test2<'a, T : ?Sized>(&'a T) where Indirect<T>: CopyImpled<Impled = False>;

type A = Test1<i32>; // why no errors here

pub fn main() {
    let a: A; // reject the code perfect
    // type mismatch resolving 
    // `<Indirect<i32> as CopyImpled>::Impled == False`
    // expected `False`, found `True`

    struct NonCopy;
    let s1 = Test1(NonCopy); // should work, but fail
    let s2 = Test2(&NonCopy); // should work, but fail
    // expected enum `False`
    // found associated type `<Indirect<NonCopy> as CopyImpled>::Impled`
}

specialization is unsound and you should not use it.

2 Likes

I found Why does the default-associated-type trait fail to infer that Output = Self? - help - The Rust Programming Language Forum and rfcs/text/1210-impl-specialization.md at master · rust-lang/rfcs

Finally, how does default help with the hazards described above? Easy: an associated type from a blanket impl must be treated "opaquely" if it's marked default.

The true sealed trait language construct is particularly powerful when combined with specialization. It ensures that all implementations of the trait—whether they are blanket or specialized—can only be defined within the crate that declares the trait. This effectively prevents any external crate from providing its own specialized impl that could override the original crate's default behavior.

In contrast, the traditional private supertrait pattern (i.e., pub trait Trait: private::Sealed) can prevent new, non‑specialized implementations for external types, but it fails to block external specialized impls. The reason is that the private supertrait is often paired with a blanket impl<T> private::Sealed for T {} to allow external types to receive the default behavior. However, this blanket impl inadvertently gives external crates a "pass" to satisfy the supertrait, enabling them to write a more specific impl that will override the default via specialization. Thus, the private supertrait offers only a superficial barrier, while a true sealed keyword would enforce a definition‑site restriction that is immune to such override attacks.

This eliminates the type-equality ambiguity that currently plagues patterns like where Indirect<T>: CopyImpled<Impled = False>, and would render the current workaround (e.g., the Is trait trick) completely obsolete.

In my experience this is not true.

2 Likes