Inline

#200 Jun 2026

200. #[derive(Copy)] and #[inline] — Make Small Types Free to Pass Around

A two-field struct that you .clone() everywhere, behind a function the optimizer won’t inline across crates — that’s two small taxes you can stop paying. Copy deletes the move/drop bookkeeping, #[inline] lets the body fold into the caller.

This is the afternoon half of a pair with the morning’s static-dispatch bite: both are about handing the optimizer a body it can actually see through and fold into the caller.

Copy: tiny types shouldn’t need moving

For a small, plain-data struct — a couple of integers, a pair of floats — a move is just a memcpy. But without Copy, the value is moved out of its binding when you pass it, so you can’t use it again afterward, and the compiler tracks drop state for it. Derive Copy and it’s duplicated bit-for-bit instead, no move semantics, no drop glue:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#[derive(Copy, Clone, Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn shift(p: Point, dx: i32) -> Point {
    Point { x: p.x + dx, y: p.y }
}

let a = Point { x: 1, y: 2 };
let b = shift(a, 10);
// `a` is still usable — it was copied, not moved
assert_eq!(a, Point { x: 1, y: 2 });
assert_eq!(b, Point { x: 11, y: 2 });

Without Copy that shift(a, ..) would move a, and the later assert_eq!(a, ..) wouldn’t compile. You’d reach for .clone() or & borrows to work around it — friction for a type that’s cheaper to copy than a pointer.

The rule of thumb: derive Copy when the type is small (fits in a register or two) and has no heap-owning fields. String, Vec, and Box can’t be Copy — only Clone — because duplicating them bit-for-bit would alias an owned allocation.

#[inline]: the optimizer can’t see across crate walls

Within a single crate the compiler inlines freely based on its own cost model. The catch is the crate boundary: a normal (non-generic) function compiled into your library is just a symbol other crates call into. Without LTO, the calling crate only sees the signature, not the body — so it emits a real call and the optimizer can’t fold a one-line helper into the loop around it.

#[inline] ships the function’s body in the crate metadata so downstream crates can inline it:

1
2
3
4
5
6
#[inline]
pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
    a + (b - a) * t
}

assert_eq!(lerp(0.0, 10.0, 0.5), 5.0);

This matters for exactly the small, hot, public functions where the call overhead rivals the work: accessors, newtype getters, math helpers, Iterator glue. Generic functions and Copy-type constructors are already inlinable across crates (their code is monomorphized at the call site), so you mostly need #[inline] for concrete, non-generic ones.

#[inline(always)] is the stronger hammer — it overrides the cost model. Reach for it rarely, only for trivial wrappers you’ve measured, because over-inlining bloats code size and can evict the instruction cache.

Putting both on a newtype wrapper

The combination shows up constantly on zero-cost newtypes — make the value free to pass and free to call through:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#[derive(Copy, Clone, Debug, PartialEq)]
struct Meters(f64);

impl Meters {
    #[inline]
    fn as_feet(self) -> f64 {
        self.0 * 3.28084
    }
}

let d = Meters(100.0);
let ft = d.as_feet();
// `d` is Copy, so taking `self` by value didn't consume it
assert_eq!(d, Meters(100.0));
assert!((ft - 328.084).abs() < 1e-9);

as_feet takes self by value with zero guilt because Meters is Copy, and #[inline] means a downstream crate calling .as_feet() in a tight loop gets the multiply folded in directly instead of a function call.

The bottom line

For small plain-data types, #[derive(Copy)] removes move/drop overhead and the ergonomic friction that pushes you toward needless .clone()s. For small public functions, #[inline] gives downstream crates the body to fold into their own code. Both are about the same thing as static dispatch: never make the optimizer guess at something you could just hand it. Profile first — but these two are cheap wins on the hot path.