Rust Attributes
every attribute rust gives you, what each one does, when to use it, and all subtypes.
- 1. what attributes actually are
- 2.
#[inline]— code generation hint - 3.
#[must_use]— caller warning - 4.
#[derive(...)]— automatic trait impls - 5.
#[cfg(...)]— conditional compilation - 6. lint attributes —
allow,warn,deny,forbid - 7.
#[test]and#[cfg(test)] - 8.
#[allow(unused_...)]family - 9.
#[doc(...)]— documentation control - 10.
#[repr(...)]— memory layout control - 11.
#[deprecated]— API deprecation - 12.
#[cold]— branch prediction hint - 13.
#[track_caller]— better panic locations - 14.
#[non_exhaustive]— future-proof enums and structs - 15. quick reference table
1. what attributes actually are
attributes are compiler directives written above an item. they change how rustc compiles, warns about, or exposes that item — without touching your runtime logic.
#[attribute] // inner-style: applies to the item below it
#![attribute] // outer-style: applies to the whole file/module it lives in
they attach to: functions, structs, enums, fields, modules, trait impls, and the entire crate.
#![allow(dead_code)] // outer — applies to the whole crate/module
#[derive(Debug, Clone)] // inner — applies to the struct below
pub struct Queue<T> { ... }
two broad categories:
| category | what it does |
|---|---|
| built-in attributes | understood directly by rustc — inline, derive, cfg, test, etc. |
| proc-macro attributes | provided by a crate — #[tokio::main], #[serde(rename)], etc. |
this document covers built-in attributes only.
2. #[inline] — code generation hint
tells the compiler: “consider copying this function’s body into the call site instead of emitting a real function call.”
the problem it solves
every function call has overhead: save the return address, jump to the function, execute, jump back. for a 1-line wrapper this overhead can cost more than the work itself.
pub fn is_empty(&self) -> bool {
self.data.is_empty() // one field access — jump overhead is wasteful
}
more importantly, inlining lets the compiler see the body at the call site and run further optimisations — dead code elimination, constant folding, removing unreachable panic branches.
cross-crate visibility — the real reason it exists
within the same crate the compiler inlines aggressively on its own. the reason you add #[inline] explicitly is for library crates: by default the compiler only ships the compiled artifact, not the function body. downstream crates can’t inline what they can’t see.
#[inline] tells the compiler: “embed this function’s IR in the .rlib metadata” so callers across the crate boundary can inline it.
without #[inline]: caller ──JUMP──> [body in graph-collections.rlib]
with #[inline]: caller [body pasted here, no jump]
the three levels
#[inline] // hint — "please consider it, compiler decides"
#[inline(always)] // force — "always inline no matter what"
#[inline(never)] // forbid — "never inline, ever"
#[inline] — right default for small wrappers. the compiler weighs binary size vs speed and decides.
#[inline(always)] — overrides LLVM’s heuristics. only use when you’ve profiled and confirmed the compiler is making the wrong call. misuse bloats the binary and can slow things down via instruction cache pressure.
#[inline(never)] — keeps the function as a real call. useful for cold error paths you want out of the hot path, or for functions used as function pointers (inlining them would be pointless anyway).
cost of inlining
inlining is not free. the tradeoff:
| no inline | inline | |
|---|---|---|
| jump overhead | yes | none |
| binary size | one copy | one copy per call site |
| further optimisations | limited | unlocked |
| compile time | faster | slower |
when to use which
is the function body 1–5 lines, just delegating?
│
├── YES → #[inline]
│ (accessors, constructors, wrappers over std types)
│
└── NO → leave it alone, trust the compiler
unless profiling shows a specific hot spot
goes as:
// good candidates
#[inline] pub fn is_empty(&self) -> bool { self.data.is_empty() }
#[inline] pub fn len(&self) -> usize { self.data.len() }
#[inline] pub fn enqueue(&mut self, e: T) { self.data.push_back(e) }
// bad candidates — complex logic, let compiler decide
pub fn from_str(s: &str) -> Result<Self, ParseError> { ... }
pub fn rebalance(&mut self) { ... }
3. #[must_use] — caller warning
a compile-time warning to the programmer. zero effect on generated machine code. it says: “if you call this and throw away the return value, that is almost certainly a bug.”
#[must_use]
pub fn len(&self) -> usize {
self.data.len()
}
if a caller writes:
queue.len(); // forgot to use the result
rustc emits:
warning: unused return value of `Queue::len` that must be used
--> src/main.rs:5:5
|
5 | queue.len();
| ^^^^^^^^^^^
adding a custom message
#[must_use = "call dequeue() or front() to actually use this queue"]
pub struct Queue<T> { ... }
the message appears in the warning and explains why the return value matters.
on structs and enums
when placed on a type, #[must_use] triggers whenever a function returns that type and the caller discards the return:
#[must_use]
pub struct Queue<T> { ... }
fn make_queue() -> Queue<u32> { Queue::new() }
make_queue(); // warning: unused `Queue` that must be used
this catches bugs like forgetting to store a builder result.
on traits
placing it on a trait method means any impl of that method inherits the warning:
pub trait Builder {
#[must_use]
fn build(self) -> Product;
}
the three places you use it
| place | when |
|---|---|
| function | return value is almost always needed (query methods, constructors that return results) |
| struct/enum | the type itself represents a deferred action or result that must be consumed |
| trait method | all impls of the method should warn if discarded |
what already has #[must_use] in std
Result<T, E>, Option<T>, and many iterator methods are #[must_use] in the standard library. that’s why let _ = some_result; silences the warning — you’re explicitly acknowledging the discard.
silencing the warning intentionally
let _ = queue.len(); // explicit discard — no warning
let _len = queue.len(); // assigned but ignored — no warning
drop(some_must_use_value); // also silences it
4. #[derive(...)] — automatic trait impls
instructs the compiler to generate a trait implementation automatically from the structure of your type.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct Queue<T> {
data: VecDeque<T>,
}
all standard derivable traits
| trait | what it generates | requires on fields |
|---|---|---|
Debug |
{:?} formatter |
all fields: Debug |
Clone |
.clone() — deep copy |
all fields: Clone |
Copy |
bitwise copy, no move semantics | all fields: Copy |
PartialEq |
== and != |
all fields: PartialEq |
Eq |
total equality marker (no extra methods) | all fields: Eq, PartialEq |
PartialOrd |
<, >, <=, >= (partial) |
all fields: PartialOrd |
Ord |
total ordering, .sort() works |
all fields: Ord, Eq |
Hash |
usable as HashMap key |
all fields: Hash |
Default |
Type::default() zero/empty value |
all fields: Default |
rules and gotchas
derive generates based on fields — for PartialEq, two instances are equal only if every field is equal. if you need custom equality logic, implement the trait manually instead.
Copy requires Clone — always derive both together:
#[derive(Copy, Clone)] // Copy requires Clone to also be implemented
pub struct Point { x: f32, y: f32 }
Eq requires PartialEq — Eq is a marker trait with no methods. it asserts that equality is total (reflexive, symmetric, transitive). always derive both:
#[derive(PartialEq, Eq)]
Ord requires PartialOrd and Eq:
#[derive(PartialEq, Eq, PartialOrd, Ord)]
Copy is not for heap-owning types — String, Vec, Box cannot be Copy. if any field owns heap data, drop Copy.
third-party derivable traits
many crates add their own derivable traits via proc-macros:
#[derive(Serialize, Deserialize)] // serde
#[derive(Error)] // thiserror
#[derive(Parser)] // clap
#[derive(Arbitrary)] // proptest / arbitrary
these work identically — the crate provides the proc-macro, you derive it.
5. #[cfg(...)] — conditional compilation
removes code at compile time based on conditions. the removed code doesn’t exist in the binary at all — unlike a runtime if.
#[cfg(target_os = "windows")]
fn platform_init() { ... } // only compiled on Windows
#[cfg(not(target_os = "windows"))]
fn platform_init() { ... } // compiled everywhere else
all built-in cfg predicates
// operating system
#[cfg(target_os = "linux")]
#[cfg(target_os = "macos")]
#[cfg(target_os = "windows")]
// os family
#[cfg(unix)] // linux, macos, bsd, etc.
#[cfg(windows)]
// architecture
#[cfg(target_arch = "x86_64")]
#[cfg(target_arch = "aarch64")]
#[cfg(target_arch = "wasm32")]
// pointer width
#[cfg(target_pointer_width = "64")]
#[cfg(target_pointer_width = "32")]
// endianness
#[cfg(target_endian = "little")]
#[cfg(target_endian = "big")]
// feature flags (from Cargo.toml [features])
#[cfg(feature = "json")]
#[cfg(feature = "async")]
// debug vs release
#[cfg(debug_assertions)] // true in dev, false in release
// test builds
#[cfg(test)] // true only during cargo test
// custom flags set via rustc --cfg
#[cfg(my_custom_flag)]
combining predicates
#[cfg(all(unix, target_arch = "x86_64"))] // AND
#[cfg(any(windows, macos))] // OR
#[cfg(not(windows))] // NOT
#[cfg(all(feature = "async", not(wasm)))] // complex combinations
cfg_attr — conditional attribute application
applies another attribute only when a condition is true:
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Queue<T> { ... }
// only derives Serialize/Deserialize when the "serde" feature is enabled
cfg!() macro — runtime boolean (use sparingly)
if cfg!(debug_assertions) {
println!("debug build");
}
unlike #[cfg(...)], cfg!() doesn’t remove the dead branch — the compiler just sees a constant true or false. it will usually optimise it away, but #[cfg(...)] is cleaner and more explicit for conditional compilation.
6. lint attributes — allow, warn, deny, forbid
control how rustc and clippy report lints on a specific item or scope.
#[allow(dead_code)] // silence this lint
#[warn(missing_docs)] // emit a warning (default for most lints)
#[deny(unsafe_code)] // make this lint a hard error
#[forbid(unsafe_code)] // hard error AND can't be overridden below this scope
scope rules
lint attributes apply to the item they’re attached to and everything inside it:
#[allow(dead_code)]
mod internal {
fn unused_fn() {} // no warning — covered by mod's allow
fn another() {} // no warning — also covered
}
fn outside() {} // warning if unused — not covered
applied at crate root with #![] they affect the whole crate:
// src/lib.rs
#![deny(missing_docs)] // every public item must have a doc comment
#![allow(dead_code)] // silence dead code warnings crate-wide
lint levels hierarchy
forbid > deny > warn > allow
a forbid at an outer scope cannot be weakened by an allow or warn in an inner scope. deny can be weakened. this is the key difference between the two.
#![forbid(unsafe_code)]
mod ffi {
#[allow(unsafe_code)] // ERROR — cannot override forbid
unsafe fn raw() { ... }
}
common lints to know
// rustc lints
#[allow(dead_code)] // unused functions, fields, variants
#[allow(unused_variables)] // assigned but never read
#[allow(unused_imports)] // imported but never used
#[allow(unused_must_use)] // discarding a #[must_use] value explicitly
#[deny(missing_docs)] // public items without doc comments
#[deny(unsafe_code)] // any use of unsafe{}
#[allow(non_snake_case)] // for FFI bindings with C naming conventions
#[allow(clippy::too_many_arguments)] // clippy-specific
// useful clippy lints to deny in libraries
#[deny(clippy::unwrap_used)] // forces you to handle None/Err explicitly
#[deny(clippy::panic)] // no panics in library code
#[deny(clippy::expect_used)] // same family as unwrap_used
suppressing a lint for one line — use a tighter scope
#[allow(clippy::unwrap_used)]
pub fn force_parse(s: &str) -> u32 {
s.parse().unwrap() // allowed here because we documented the invariant
}
7. #[test] and #[cfg(test)]
marks a function as a unit test. only compiled and run during cargo test.
#[test]
fn addition_works() {
assert_eq!(2 + 2, 4);
}
#[cfg(test)] on a module
the standard pattern — gates an entire module so it only exists in test builds:
#[cfg(test)]
mod tests {
use super::*; // bring parent module into scope
#[test]
fn enqueue_dequeue() {
let mut q = Queue::new();
q.enqueue(1);
assert_eq!(q.dequeue(), Some(1));
}
}
without #[cfg(test)] on the module, the module still compiles in non-test builds — just none of the functions inside run. #[cfg(test)] removes it entirely from release builds.
#[should_panic]
asserts that the test function panics. fails if it does not panic.
#[test]
#[should_panic]
fn out_of_bounds_panics() {
let v: Vec<i32> = vec![];
let _ = v[0]; // must panic for test to pass
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn panics_with_message() {
let v: Vec<i32> = vec![];
let _ = v[0]; // panic message must contain the expected string
}
#[ignore]
marks a test to be skipped by default. runs only with cargo test -- --ignored or cargo test -- --include-ignored.
#[test]
#[ignore = "requires network access, run manually"]
fn integration_with_api() { ... }
useful for slow tests, tests needing external services, or tests that are known-failing and tracked elsewhere.
8. #[allow(unused_...)] family
a specific subgroup of lint attributes you’ll use constantly, especially during development.
#[allow(dead_code)] // fn or struct defined but never called/constructed
#[allow(unused_variables)] // let x = 5; but x is never read
#[allow(unused_mut)] // let mut x = 5; but x is never mutated
#[allow(unused_imports)] // use std::fmt; but fmt never used
#[allow(unused_assignments)] // x = 5; value assigned but never read before reassign
the _ prefix alternative
a cleaner way than #[allow] for local variables — prefix with underscore:
let _unused = expensive_computation(); // no warning, communicates intentional discard
let _ = result; // discard without binding a name at all
when to actually suppress vs fix
dead_code in a library → usually a real bug, fix it
dead_code during dev → allow temporarily, remove later
unused_variables in → use _ prefix instead of allow
a function parameter
non_snake_case → legitimate for FFI, use allow
9. #[doc(...)] — documentation control
controls how rustdoc renders and generates documentation for an item.
basic doc comments (not an attribute, but related)
/// single-line doc comment — becomes HTML in rustdoc
/// supports **markdown**, `code spans`, and [links]
/** multi-line doc comment
also valid, less common */
//! inner doc comment — documents the module/crate it's inside of
#[doc = "..."] — programmatic doc strings
identical to /// but useful when generated by macros:
#[doc = "Creates a new empty queue."]
pub fn new() -> Self { ... }
#[doc(hidden)]
excludes the item from the public documentation. the item is still public and usable — it just doesn’t appear in rustdoc output. use for implementation details that must be pub for technical reasons but aren’t part of the stable API.
#[doc(hidden)]
pub fn __internal_impl_detail() { ... }
#[doc(alias = "...")]
adds a search alias in rustdoc. users searching for the alias will find this item even though it has a different name:
#[doc(alias = "push")]
pub fn enqueue(&mut self, element: T) { ... }
// someone searching "push" finds enqueue
#[doc(cfg(...))]
shows a badge in rustdoc indicating this item is only available under certain cfg conditions:
#[cfg(feature = "async")]
#[doc(cfg(feature = "async"))]
pub async fn drain(&mut self) { ... }
// rustdoc shows: "Available on feature="async" only"
10. #[repr(...)] — memory layout control
controls the memory representation of a struct or enum. matters for FFI, unsafe code, and performance-sensitive layout.
#[repr(C)] // C-compatible layout — fields in declaration order, C alignment rules
#[repr(Rust)] // default — compiler can reorder fields for optimal packing
#[repr(transparent)]// single-field struct/enum has same layout as the inner type
#[repr(packed)] // remove all padding between fields — alignment = 1
#[repr(align(N))] // enforce minimum alignment of N bytes (N must be power of 2)
#[repr(u8)] // enum discriminant stored as u8
#[repr(u16)] // enum discriminant stored as u16
#[repr(u32)] // etc.
#[repr(i8)] // signed variants also exist
when each matters
#[repr(C)] — required when passing a struct to C code via FFI. Rust’s default layout can reorder and pad fields in any way it likes, which breaks C ABI expectations.
#[repr(C)]
pub struct Point { x: f32, y: f32 } // safe to pass to a C function
#[repr(transparent)] — for newtype wrappers. guarantees the wrapper has identical layout to the inner type. required when using the newtype in FFI in place of the inner type.
#[repr(transparent)]
pub struct Meters(f64); // exactly the same layout as f64
#[repr(u8)] on enums — controls the size of the tag/discriminant:
#[repr(u8)]
enum Direction { North = 0, South = 1, East = 2, West = 3 }
// discriminant is stored as a single byte
#[repr(packed)] — removes padding. makes structs smaller but accessing misaligned fields is undefined behaviour on some architectures. use only for serialisation formats where byte layout must match exactly.
#[repr(align(N))] — forces over-alignment. used for SIMD types, cache-line alignment, or hardware requirements.
#[repr(align(64))]
struct CacheLinePadded { data: [u8; 64] } // starts on a cache line boundary
11. #[deprecated] — API deprecation
marks an item as deprecated. anyone using it gets a compiler warning. required for semver-compatible API evolution in published crates.
#[deprecated]
pub fn old_push(&mut self, val: T) { ... }
#[deprecated(since = "0.3.0", note = "use enqueue() instead")]
pub fn push(&mut self, val: T) {
self.enqueue(val)
}
since — semver version string when the deprecation was introduced. shown in docs.
note — tells users what to use instead. always include this.
the deprecated function still compiles and works — the warning just nudges users to migrate. to silence it intentionally:
#[allow(deprecated)]
old_api::push(&mut queue, val);
12. #[cold] — branch prediction hint
marks a function as unlikely to be called. the compiler moves its generated code away from hot paths, improving instruction cache usage for the common case.
#[cold]
fn handle_oom(layout: Layout) -> ! {
panic!("allocation failed: {:?}", layout)
}
pair with #[inline(never)] for error/panic paths to keep them completely out of the hot path:
#[cold]
#[inline(never)]
fn index_out_of_bounds(len: usize, index: usize) -> ! {
panic!("index {index} out of bounds for length {len}");
}
13. #[track_caller] — better panic locations
when a function panics, the error message normally points to the line inside the function. #[track_caller] makes it point to the call site instead — where the caller invoked the function.
#[track_caller]
pub fn expect_non_empty(&self) -> &T {
self.front().expect("queue was empty")
// panic message will show the caller's location, not this line
}
without it:
panicked at 'queue was empty', src/queue.rs:42
with it:
panicked at 'queue was empty', src/main.rs:17 ← where the caller called expect_non_empty
this is how unwrap() and expect() in std work — they use #[track_caller] so the panic points at your code, not into libcore.
14. #[non_exhaustive] — future-proof enums and structs
prevents external code from constructing or exhaustively matching the type. you can add new variants/fields in a future version without it being a breaking change.
#[non_exhaustive]
pub enum Error {
Io(io::Error),
Parse(ParseError),
// we can add variants later without breaking semver
}
external code must add a wildcard arm:
match err {
Error::Io(e) => ...,
Error::Parse(e) => ...,
_ => ..., // required — compiler enforces this
}
on structs it prevents external construction with struct literal syntax:
#[non_exhaustive]
pub struct Config {
pub timeout: Duration,
pub retries: u32,
// can add fields later
}
// external code cannot do:
let c = Config { timeout: Duration::from_secs(5), retries: 3 }; // ERROR
// must use a constructor you provide:
let c = Config::default();
use it on any public type in a library when you expect the type to grow over time.
15. quick reference table
| attribute | affects machine code | affects compiler warnings | use when |
|---|---|---|---|
#[inline] |
yes — copy body to call site | no | small delegation functions in library crates |
#[inline(always)] |
yes — forced | no | profiled hot spots only |
#[inline(never)] |
yes — forced no-inline | no | cold paths, function pointers |
#[must_use] |
no | yes — warn on discard | return value is almost always needed |
#[derive(...)] |
yes — generates impls | no | standard traits on your types |
#[cfg(...)] |
yes — removes code | no | platform/feature conditional code |
#[allow(...)] |
no | yes — silence lint | intentional exceptions to lint rules |
#[warn(...)] |
no | yes — emit warning | explicitly calling out a lint |
#[deny(...)] |
no | yes — hard error | enforcing code quality in a scope |
#[forbid(...)] |
no | yes — unoverridable error | crate-level safety guarantees |
#[test] |
yes — only in test builds | no | unit test functions |
#[should_panic] |
no | no | tests that must panic to pass |
#[ignore] |
no | no | slow/external tests skipped by default |
#[doc(hidden)] |
no | no | public-but-internal API items |
#[doc(alias)] |
no | no | search aliases in rustdoc |
#[repr(C)] |
yes — changes layout | no | FFI structs |
#[repr(transparent)] |
yes — enforces layout | no | newtype wrappers |
#[repr(u8/i8/...)] |
yes — discriminant size | no | enums with explicit tag size |
#[deprecated] |
no | yes — warn on use | removing old API with migration path |
#[cold] |
yes — moves to cold path | no | error/panic handlers |
#[track_caller] |
no | no | panic messages pointing at call site |
#[non_exhaustive] |
no | no | public enums/structs that will grow |