Question
How can I make an expression such as vec << 4 append 4 to a Vec in Rust? I attempted to implement the standard library's Shl trait for the standard library's Vec<T> type:
use std::ops::Shl;
impl<T> Shl<T> for Vec<T> {
type Output = Vec<T>;
fn shl(&self, elem: &T) -> Vec<T> {
self.push(*elem);
*self
}
}
fn main() {
let v = vec![1, 2, 3];
v << 4;
}
However, Rust reports that an implementation cannot be provided when both the trait and the type are defined outside the current crate. Do I need to modify the standard library, or is there a way to make this compile successfully?
Short Answer
Rust's orphan rule prevents a crate from implementing an external trait, such as std::ops::Shl, for an external type, such as Vec<T>. The normal solution is to define a local wrapper type—called a newtype—around Vec<T>, then implement the operator trait for that wrapper. You will also learn when << and <<= are appropriate and how operator implementations consume or mutate values.
Concept
Rust uses traits to define shared behavior. For example, the << operator is defined by std::ops::Shl, and the <<= operator is defined by std::ops::ShlAssign.
Rust has a coherence rule commonly called the orphan rule:
You may implement a trait for a type only when your crate defines the trait, the type, or both.
Therefore, this is rejected:
use std::ops::Shl;
impl<T> Shl<T> for Vec<T> {
// Not allowed: both Shl and Vec come from the standard library.
type Output = Vec<T>;
fn shl(self, rhs: T) -> Vec<T> {
self
}
}
Your crate owns neither Shl nor Vec<T>. If this were allowed, separate crates could define conflicting meanings for the same operation. One crate might make Vec << value append an item, while another might make it remove an item. Rust must guarantee that every trait/type combination has one unambiguous implementation.
You do not need to patch the standard library. Instead, create a type that your crate owns and wrap the vector inside it. Since the wrapper is local, you may implement standard traits for it.
There is also a separate issue in the attempted implementation: Shl normally receives self and the right-hand operand by value. Mutating a vector requires mutable access, and returning the original vector by value usually means consuming self. For an in-place operation, ShlAssign is usually the better operator trait.
Mental Model
Think of a trait implementation as a rule added to a shared rulebook.
Shlis a rulebook page owned by the Rust standard library.Vec<T>is an object design owned by the Rust standard library.- Your crate cannot write a new rule that changes how two other owners' things interact.
A newtype is like putting the vector into a box with your own label:
struct PushVec<T>(Vec<T>);
You own PushVec<T>, so you can decide what PushVec << item means. The vector still stores the data, but your wrapper provides your crate's custom behavior.
Syntax and Examples
Shl defines the << operator. Its simplified shape is:
use std::ops::Shl;
trait Shl<Rhs = Self> {
type Output;
fn shl(self, rhs: Rhs) -> Self::Output;
}
For a custom vector wrapper, << can consume the wrapper, append an item, and return the updated wrapper:
use std::ops::Shl;
#[derive(Debug)]
struct PushVec<T>(Vec<T>);
impl<T> Shl<T> for PushVec<T> {
type Output = Self;
fn shl(mut self, elem: T) -> Self {
self.0.push(elem);
self
}
}
fn main() {
let values = PushVec(vec![1, 2, 3]);
let values = values << 4;
println!("{:?}", values.0); // [1, 2, 3, 4]
}
The mut self parameter means the method owns a mutable local copy of the wrapper. It appends to the inner Vec<T> and returns the wrapper.
For syntax that directly communicates mutation, implement ShlAssign for <<= instead:
use std::ops::ShlAssign;
#[derive(Debug)]
struct PushVec<T>(Vec<T>);
impl<T> ShlAssign<T> for PushVec<T> {
fn shl_assign(&mut self, elem: T) {
self.0.push(elem);
}
}
fn main() {
let mut values = PushVec(vec![1, 2, 3]);
values <<= 4;
println!("{:?}", values.0); // [1, 2, 3, 4]
}
<<= is often a clearer choice because it modifies an existing value, much like +=.
Step by Step Execution
Consider this ShlAssign implementation:
use std::ops::ShlAssign;
struct PushVec<T>(Vec<T>);
impl<T> ShlAssign<T> for PushVec<T> {
fn shl_assign(&mut self, elem: T) {
self.0.push(elem);
}
}
fn main() {
let mut values = PushVec(vec![10, 20]);
values <<= 30;
}
Execution proceeds as follows:
-
valuesis created asPushVec(vec![10, 20]). -
values <<= 30is operator syntax that Rust translates conceptually to:ShlAssign::shl_assign(&mut values, 30); -
The
&mut valuesborrow givesshl_assignpermission to modifyvalues. -
self.0accesses the wrappedVec<T>. -
push(elem)adds30to that vector. -
After the statement,
values.0contains[10, 20, 30].
In contrast, values << 30 calls Shl::shl(values, 30). That form usually consumes values and produces a new result, which you should assign or otherwise use.
Real World Use Cases
The orphan rule and newtype pattern appear in many practical Rust programs:
- Domain identifiers: Wrap
Stringoru64in types such asUserId,EmailAddress, orOrderIdso unrelated values cannot be mixed accidentally. - Validated values: Wrap a
StringinNonEmptyNameafter verifying it is not empty. - Custom serialization: Wrap an external type when you need a custom
serderepresentation but cannot directly implement an external serialization trait for that external type. - Units of measurement: Use
Meters(f64)andSeconds(f64)to make invalid calculations harder to write. - Application-specific operators: A graphics, math, or query-builder library may use operator traits on local wrapper types when the meaning is intuitive to its users.
- Collection APIs: A wrapper can expose restricted operations, logging, capacity limits, validation, or custom insertion behavior around a
Vec<T>.
Real Codebase Usage
In production Rust code, a newtype often provides more than a legal trait implementation. It creates a boundary where an application can control invariants and its public API.
A common pattern is to provide normal named methods in addition to any operator overload:
#[derive(Debug, Default)]
struct PushVec<T>(Vec<T>);
impl<T> PushVec<T> {
fn new() -> Self {
Self(Vec::new())
}
fn push(&mut self, value: T) {
self.0.push(value);
}
fn into_inner(self) -> Vec<T> {
self.0
}
}
Named methods are usually clearer than unusual operator meanings. push immediately tells readers what happens; << may be surprising for a vector because Rust commonly uses it for bit shifting and stream-like APIs.
Developers also commonly implement conversion traits for ergonomic boundaries:
impl<T> From<Vec<T>> for PushVec<T> {
fn from(values: Vec<T>) -> Self {
Self(values)
}
}
impl<T> From<PushVec<T>> for Vec<T> {
fn from(values: PushVec<T>) -> Self {
values.0
}
}
Avoid automatically implementing Deref<Target = Vec<T>> unless the wrapper should truly behave like a vector everywhere. Deref can expose methods that bypass the rules or invariants the wrapper was meant to enforce.
Common Mistakes
Implementing an external trait for an external type
use std::ops::Shl;
impl<T> Shl<T> for Vec<T> {
type Output = Vec<T>;
fn shl(self, item: T) -> Vec<T> {
self
}
}
This fails due to the orphan rule. Define a local wrapper such as struct PushVec<T>(Vec<T>); and implement the trait for that wrapper.
Using the wrong Shl method signature
This does not match the Shl trait:
fn shl(&self, elem: &T) -> Vec<T> {
todo!()
}
Shl<T> expects a method shaped like this:
fn shl(self, rhs: T) -> Self::Output
The exact types vary with your chosen right-hand side and output types, but the receiver is normally self, not &self.
Trying to mutate through &self
fn shl(&self, elem: T) -> Self {
self.0.push(elem); // Error: cannot mutably borrow through &self
todo!()
}
Use mut self for a consuming operator implementation, or use &mut self through ShlAssign for an in-place operator.
Moving out of a borrowed value
fn shl(&self, elem: &T) -> Vec<T> {
self.push(*elem);
*self
}
Even aside from the trait mismatch, this attempts to mutate through an immutable reference and move out of a borrowed value. It would also require T: Copy to use *elem. Prefer receiving the element by value when the operator should take ownership of it.
Ignoring the output of <<
let values = PushVec(vec![1, 2, 3]);
values << 4;
If Shl consumes and returns Self, the returned updated value must be used:
let values = values << 4;
If you want to keep and mutate the same binding, use <<= with a mutable variable.
Overloading operators with surprising meanings
Operator overloads should be easy to predict. If readers are likely to confuse << with a bit shift, a method such as push, append, or insert is usually more maintainable.
Comparisons
| Choice | Syntax | Ownership behavior | Best use |
|---|---|---|---|
Vec::push | values.push(4) | Mutably borrows the existing vector | The standard, clearest way to append one item |
Shl | let values = values << 4 | Usually consumes the left value and returns an output | Builder-style transformations where the operator meaning is clear |
ShlAssign | values <<= 4 | Mutably borrows and updates the left value | Custom in-place operator syntax |
| Newtype wrapper | PushVec<T>(Vec<T>) | Creates a distinct type | Adding trait implementations, validation, or domain meaning |
| Type alias | type PushVec<T> = Vec<T> | Is exactly the same type as Vec<T> | Shortening a type name; cannot bypass the orphan rule |
A type alias does not create a new type, so it cannot solve this problem:
type PushVec<T> = Vec<T>;
Only a tuple struct, normal struct, or enum creates a genuinely local type that can receive the implementation.
Cheat Sheet
-
Orphan rule: implement a trait only if your crate owns the trait or the type.
-
std::ops::Shlpowersleft << right. -
std::ops::ShlAssignpowersleft <<= right. -
You cannot write
impl Shl<T> for Vec<T>in an ordinary application crate. -
Create a local wrapper:
struct PushVec<T>(Vec<T>); -
Consuming
<<implementation:impl<T> std::ops::Shl<T> for PushVec<T> { type Output = Self; fn shl(mut self, rhs: T) -> Self { self.0.push(rhs); self } } -
In-place
<<=implementation:impl<T> std::ops::ShlAssign<T> for PushVec<T> { fn shl_assign(&mut self, rhs: T) { self.0.push(rhs); } } -
Prefer
Vec::pushwhen custom operator syntax does not improve readability. -
A
type Alias = Vec<T>is not a new type and does not help.
FAQ
Why does Rust have the orphan rule?
It prevents conflicting trait implementations across crates. This lets Rust select implementations predictably and keeps generic code coherent.
Can I implement a standard library trait for my own type?
Yes. For example, you may implement std::ops::Shl for PushVec<T> because PushVec<T> is defined in your crate.
Can I implement my own trait for Vec<T>?
Yes. Your crate owns the trait, so this is allowed:
trait AppendOne<T> {
fn append_one(&mut self, value: T);
}
impl<T> AppendOne<T> for Vec<T> {
fn append_one(&mut self, value: T) {
self.push(value);
}
}
Why does Vec::push take &mut self but Shl::shl takes self?
They represent different API designs. push modifies an existing vector. Shl is designed for an expression that consumes operands and returns a result, though implementations can choose appropriate owned or referenced operand types.
Should I use << to append to a vector?
Usually, use push. It is conventional, immediately understandable, and already available. Use an overloaded operator only when it makes your domain API clearer.
Does a type alias bypass the orphan rule?
No. A type alias is another name for the same type. A tuple struct such as struct PushVec<T>(Vec<T>); creates a distinct type.
Can I add methods directly to Vec<T>?
No. Inherent impl blocks must also be written for types defined in your crate. Use an extension trait for additional methods or use a newtype for stronger control.
Mini Project
Description
Build a small LogBuffer wrapper around Vec<String>. It demonstrates the newtype pattern and implements <<= as a convenient in-place operation for adding log messages. This resembles application code that collects events before displaying, saving, or sending them.
Goal
Create a local collection wrapper that accepts strings through <<= and can return its collected messages.
Requirements
Define a LogBuffer newtype that stores Vec<String>. Implement ShlAssign<String> for LogBuffer. Add a method that returns the number of stored messages. Add a method that consumes the buffer and returns the inner Vec<String>. In main, add at least two messages using <<= and print the count.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.