Introduction
Unpin is an auto trait that indicates a type is safe to move even after being pinned. Most standard types implement Unpin, which means Pin has no effect on them. Understanding Unpin is key to knowing when Pin actually matters and when it is a no-op.
Key Concepts
- Unpin: An auto trait implemented by most types, indicating the type has no self-references and can be safely moved out of Pin.
- !Unpin: Types that do NOT implement Unpin — they genuinely cannot be moved once pinned. This includes async block return types and types containing PhantomPinned.
- Pin<&mut T> where T: Unpin: Equivalent to
&mut T— Pin has no effect because the type opts into being movable.
Real World Context
When writing manual Future implementations or building async combinators, you encounter Pin<&mut Self> in the poll method. For most custom futures that do not hold self-references, your type is Unpin and you can freely access &mut self. Only when you store other !Unpin futures (like async block results) do you need to handle Pin projection carefully.
Deep Dive
Most types implement Unpin automatically:
rustuse std::marker::Unpin; fn is_unpin<T: Unpin>() {} is_unpin::<i32>(); // OK is_unpin::<String>(); // OK is_unpin::<Vec<i32>>(); // OK
For Unpin types, Pin<&mut T> is equivalent to &mut T. You can freely extract the inner reference:
rustuse std::pin::Pin; let mut x = 5; let pinned: Pin<&mut i32> = Pin::new(&mut x); // Can get mutable reference because i32: Unpin let r: &mut i32 = Pin::into_inner(pinned); *r = 10;
Types that are !Unpin genuinely cannot be moved:
- Async function/block return types
- Types containing
PhantomPinned - Self-referential types
rustuse std::marker::PhantomPinned; struct NotUnpin { data: i32, _pin: PhantomPinned, // Makes struct !Unpin } // Can't get &mut NotUnpin from Pin<&mut NotUnpin> // Can't move out of Pin<Box<NotUnpin>>
Async blocks capture local variables that may reference each other across await points:
rustasync fn has_self_ref() { let data = [1, 2, 3]; let slice = &data[..]; // Reference into local data tokio::task::yield_now().await; // Suspension point println!("{:?}", slice); // Still uses the reference } // The generated Future contains both data AND slice // Moving it would invalidate slice — hence it's !Unpin
Common Pitfalls
- Assuming all futures are !Unpin — Simple futures that don't hold cross-await references may be Unpin. Only futures with self-references are !Unpin.
- Fighting Pin when your type is Unpin — If your custom type implements Unpin (the default), you can use
Pin::into_inner()to get a regular&mutreference.
Best Practices
- Check if your type is Unpin before adding Pin complexity — Use
fn assert_unpin<T: Unpin>() {}to verify at compile time. - Use Box::pin to erase !Unpin concerns —
Box::pin(future)returnsPin<Box<F>>, which is always valid regardless of whether F is Unpin.
Summary
- Most types are Unpin, meaning Pin has no effect on them.
- Async block return types are !Unpin because they may contain self-references.
- Pin<&mut T> where T: Unpin is equivalent to &mut T.
- PhantomPinned makes a type !Unpin, opting into genuine move prevention.
Code Examples
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
// Most types: Unpin — Pin has no special effect
struct SimpleFuture;
impl Future for SimpleFuture {
type Output = i32;
fn poll(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<i32> {
Poll::Ready(42)
}
}
// Self-referential: !Unpin — Pin prevents moving
struct ComplexFuture {
data: String,
slice: *const str,
_pin: std::marker::PhantomPinned,
}
// For !Unpin, poll() is the only way to access &mut self
// This prevents accidentally moving the future