What an attribute is¶
Level: 201 · working knowledge
One line: An attribute is metadata the compiler acts on — five families cover almost all of them, and the only piece of syntax to learn is that #! applies to the thing it is inside while #[ applies to the thing below it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
struct Score(u8);
fn main() {
let a = Score(4);
println!("{a:?} {}", a == Score(4)); // Score(4) true
}
Eight traits, no bodies. Each one is a real impl block the compiler wrote for you.
The five families¶
| Family | Examples | Does |
|---|---|---|
| derive | #[derive(Debug, Clone)] |
generate an impl |
| lints | #[allow], #[warn], #[deny], #[forbid] |
change what a diagnostic does |
| cfg | #[cfg(test)], #[cfg(unix)] |
include or exclude the item entirely |
| test | #[test], #[bench], #[ignore] |
mark a function for the test harness |
| codegen / API | #[inline], #[repr(C)], #[must_use], #[non_exhaustive], #[deprecated] |
change layout, hints, or the contract |
A doc comment is one too: /// is sugar for #[doc = "…"], and //! for #![doc = "…"]. That is why a doc comment in the wrong place produces an attribute error rather than a comment error.
Inner and outer¶
#![allow(clippy::needless_return)] // applies to the whole file it opens
#[allow(dead_code)] // applies to the item below
fn never_called() -> u32 { 0 }
The ! is the whole difference, and an inner attribute has to be the first thing in its file or block. //! and /// follow the same rule for the same reason.
The lint levels, and the one to avoid¶
allow |
say nothing |
warn |
print, keep compiling |
deny |
error |
forbid |
error, and an inner allow is itself an error |
They nest, innermost wins — except forbid, which cannot be relaxed. So #![forbid(unsafe_code)] at the crate root is a real promise worth making, and #![forbid(warnings)] is a trap: the day one generated macro trips a new lint, nobody can allow it locally. Deny for style, forbid for safety properties.
#[cfg] deletes; cfg!() chooses¶
#[cfg(test)]
mod tests {
#[test]
fn it_works() { assert_eq!(2 + 2, 4); }
}
fn main() {
if cfg!(debug_assertions) { println!("debug build"); }
}
#[cfg(test)] means the module does not exist in a normal build — not that it is skipped at run time, which is why test code costs a release binary nothing. cfg!(…) is an expression that becomes a literal true or false, and both branches still have to type-check.
The trap: derived Ord reads your field order¶
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct ByName { name: &'static str, score: u32 }
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct ByScore { score: u32, name: &'static str }
Same two fields, same derive, and sorting the same three records gives ["Ada", "Ben", "Cara"] for one and ["Cara", "Ben", "Ada"] for the other. Derived comparison is lexicographic in declaration order, so moving a field up is a one-line diff that reads as cosmetic and silently reorders every sort, every BTreeMap and every binary_search over the type.
If the order matters, write the impl Ord — then the diff that changes it is the one that says so.
Two more derive rules worth knowing before they bite: #[derive(Default)] on an enum needs a variant marked #[default] (E0665), and every derive adds a bound on the type parameters, so #[derive(Clone)] on Wrapper<T> produces impl<T: Clone> Clone for Wrapper<T> — which is why a Wrapper<NotClone> sometimes refuses to clone for reasons that are not in your code.
If you are coming from another language¶
- Python. Decorators are the closest thing, and the resemblance is real for
#[test]— but@decoratoris a function called at run time that returns a new object, while an attribute is read by the compiler and can delete the item entirely.#[cfg(test)]has no decorator equivalent at all; the nearest Python idiom is anif TYPE_CHECKING:block or a# pragma: no cover, both of which are conventions read by other tools rather than by the interpreter.@dataclassis the honest analogue of#[derive]: both generate the methods you would have typed, and both generate__eq__/__lt__comparing fields in declaration order — so the trap above is a@dataclass(order=True)trap too, with the same fix. - ABAP. Pragmas (
##NEEDED,##NO_TEXT) and pseudo-comments ("#EC NOTEXT) are the lint family exactly: metadata that silences a specific check at a specific place, read by the Code Inspector rather than by the runtime.#[allow(dead_code)]is##NEEDED. What ABAP has no counterpart for isderive— aREDEFINITIONofIS_EQUALor aGET_TEXThas to be written out — and none for#[cfg], since there is no compile-time variant selection; the nearest thing is aIF sy-sysid = 'PRD'check, which is a run-time branch and stays in the shipped code. - Java / C#. Annotations and attributes, and the naming is nearly the same. The difference is what they do: a Java annotation is metadata read reflectively at run time (or by an annotation processor at build time), while Rust's are compiler instructions with no run-time trace.
@Deprecatedand#[deprecated]are the closest exact match. - C / C++.
#ifdefis#[cfg], and#pragma/[[attributes]]are the rest. Rust's version type-checks the excluded branch's syntax — it must still parse — where#ifdefcan hide anything at all.
The verified output¶
Verified output of what_an_attribute_is.rs — regenerated by tools/run_examples.py, never hand-typed.
1. `derive` writes the impl you would have written
Debug: Score(4)
Copy: a is still usable after `let b = a`: Score(4) Score(4)
PartialEq: a == b is true
Ord: sorted [4, 1, 3] -> [Score(1), Score(3), Score(4)]
Default: Score::default() = Score(0)
Eight traits, no bodies. Each one is a real impl block the
compiler generated, and each has a rule: derived Ord compares
fields in DECLARATION ORDER, so reordering a struct's fields is
a behaviour change.
2. Inner and outer, and how to tell them apart
#[attr] applies to the item BELOW it (outer)
#![attr] applies to the item it is INSIDE (inner)
The `!` is the whole difference, and an inner attribute has to be
the first thing in its file or block. `//!` and `///` follow the
same rule, because a doc comment IS an attribute — `#[doc = "…"]`.
3. The lint family: allow, warn, deny, forbid
#[allow(dead_code)] on never_called() is why this program has no
warnings. The four levels differ in what they do and in whether
an inner scope may override them:
allow say nothing
warn print, keep compiling
deny error
forbid error, and an inner allow is itself an error
They nest, so a crate-level `#![deny(warnings)]` can be relaxed
on one function — which is the whole reason to prefer deny over
forbid.
4. `cfg`: compiled or not compiled at all
#[cfg(test)] on the tests module above means it does not exist in
this binary — not that it is skipped at run time.
cfg!(test) here = false <- this binary was not built with --test
`#[cfg(…)]` removes code; `cfg!(…)` is an expression that becomes
a literal true or false. The second still type-checks both sides,
which is why a `cfg!(windows)` branch cannot rot on a Mac.
(Nothing here prints the target OS on purpose: every example in
this library has a recorded answer key, and `env::consts::OS` says
"macos" on the author's machine and "linux" in CI. That is the
determinism rule, and it is easier to break than it looks.)
5. The rest, in one line each
#[test] this fn is a test; rustc --test collects it
#[derive(…)] generate these impls
#[non_exhaustive] downstream crates may not match it exhaustively
or build it with a struct literal: Star
#[inline] a hint, not an instruction: doubled(21) = 42
#[must_use] warn if the return value is dropped
#[repr(C)] lay this type out the way C would
#[deprecated] warn at every use, with a message
Practice¶
The derive that changed behaviour when a field moved. Write the same two-field record twice, with the fields in opposite orders, derive Ord on both, and sort three of each. Explain the two different answers in one sentence, and say what you would do if the sort order is part of the type's contract.
Then three smaller ones. Derive Default for an enum and find out what it needs. Put #[must_use] on a function, discard its result, and read the warning and the fix rustc offers. And say why #![deny(warnings)] at the crate root is reasonable while #![forbid(warnings)] is not.
Solution
what_an_attribute_is_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: the derive that changed behaviour when a field moved.
//!
//! rustc --edition 2024 what_an_attribute_is_kata.rs -o /tmp/wak && /tmp/wak
/// Field order: name first.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
struct ByName {
name: &'static str,
score: u32,
}
/// The same two fields, swapped. Nothing else differs.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
struct ByScore {
score: u32,
name: &'static str,
}
#[derive(Debug, Default, Clone, PartialEq)]
struct Config {
seats: u32,
method: Method,
strict: bool,
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
enum Method {
#[default]
Star,
Approval,
}
#[must_use]
fn tally(scores: &[u32]) -> u32 {
scores.iter().sum()
}
fn main() {
println!("1. Derived Ord compares fields in declaration order");
let mut a = vec![
ByName { name: "Cara", score: 1 },
ByName { name: "Ada", score: 9 },
ByName { name: "Ben", score: 5 },
];
let mut b = vec![
ByScore { score: 1, name: "Cara" },
ByScore { score: 9, name: "Ada" },
ByScore { score: 5, name: "Ben" },
];
a.sort();
b.sort();
println!(" name first : {:?}", a.iter().map(|r| r.name).collect::<Vec<_>>());
println!(" score first: {:?}", b.iter().map(|r| r.name).collect::<Vec<_>>());
println!(" Same data, same derive, opposite answers. Moving a field up is a");
println!(" one-line diff that reads as cosmetic and silently reorders every");
println!(" sort, every BTreeMap and every binary search over the type.");
println!(" If the order matters, write the impl:");
println!(" impl Ord for ByName {{ fn cmp(&self, o: &Self) -> Ordering {{ … }} }}");
println!(" and then the diff that changes it is the one that says so.");
println!();
println!("2. `derive(Default)` on an enum needs `#[default]`");
println!(" Config::default() = {:?}", Config::default());
let approving = Config { method: Method::Approval, ..Config::default() };
println!(" with one field changed: {approving:?}");
println!(" Without `#[default]` on a variant, deriving Default for an enum is");
println!(" E0665, \"`#[derive(Default)]` on enum with no `#[default]`\" — the");
println!(" compiler cannot guess which variant is the zero. For a struct it");
println!(" can: every field's own Default.");
println!();
println!("3. `#[must_use]` turns a discarded value into a warning");
let total = tally(&[5, 3, 0]);
println!(" tally(&[5, 3, 0]) = {total}");
println!(" Writing `tally(&scores);` on its own line warns: \"unused return");
println!(" value of `tally` that must be used\", and offers `let _ = ...` as");
println!(" the way to say you meant it. It is the mechanism behind");
println!(" the warning you have already met on Result and on iterator");
println!(" adapters — `#[must_use]` sits on the TYPE there, not the method.");
println!();
println!("4. Attributes that change what is compiled, not how");
println!(" #[cfg(test)] the item does not exist in a normal build");
println!(" #[cfg(unix)] nor on Windows");
println!(" #[cfg(feature = \"x\")] nor unless the feature is on");
println!(" cfg!(unix) compiled on BOTH platforms — the branch not taken");
println!(" still had to type-check, and only then was replaced by a literal.");
println!(" #[cfg] deletes; cfg!() chooses. Which is why a cfg! branch cannot");
println!(" rot and a #[cfg] one can: nobody compiles the deleted arm.");
println!();
println!("5. The four lint levels, and the one to avoid");
println!(" allow / warn / deny / forbid, innermost wins — except `forbid`,");
println!(" which cannot be relaxed by an inner `allow` (that is itself an");
println!(" error). So a crate-wide `#![forbid(unsafe_code)]` is a real");
println!(" promise, and a crate-wide `#![forbid(warnings)]` is a trap: the");
println!(" day one generated macro trips a new lint, nobody can allow it");
println!(" locally. Use deny for style, forbid for safety properties.");
}
Verified output of what_an_attribute_is_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
1. Derived Ord compares fields in declaration order
name first : ["Ada", "Ben", "Cara"]
score first: ["Cara", "Ben", "Ada"]
Same data, same derive, opposite answers. Moving a field up is a
one-line diff that reads as cosmetic and silently reorders every
sort, every BTreeMap and every binary search over the type.
If the order matters, write the impl:
impl Ord for ByName { fn cmp(&self, o: &Self) -> Ordering { … } }
and then the diff that changes it is the one that says so.
2. `derive(Default)` on an enum needs `#[default]`
Config::default() = Config { seats: 0, method: Star, strict: false }
with one field changed: Config { seats: 0, method: Approval, strict: false }
Without `#[default]` on a variant, deriving Default for an enum is
E0665, "`#[derive(Default)]` on enum with no `#[default]`" — the
compiler cannot guess which variant is the zero. For a struct it
can: every field's own Default.
3. `#[must_use]` turns a discarded value into a warning
tally(&[5, 3, 0]) = 8
Writing `tally(&scores);` on its own line warns: "unused return
value of `tally` that must be used", and offers `let _ = ...` as
the way to say you meant it. It is the mechanism behind
the warning you have already met on Result and on iterator
adapters — `#[must_use]` sits on the TYPE there, not the method.
4. Attributes that change what is compiled, not how
#[cfg(test)] the item does not exist in a normal build
#[cfg(unix)] nor on Windows
#[cfg(feature = "x")] nor unless the feature is on
cfg!(unix) compiled on BOTH platforms — the branch not taken
still had to type-check, and only then was replaced by a literal.
#[cfg] deletes; cfg!() chooses. Which is why a cfg! branch cannot
rot and a #[cfg] one can: nobody compiles the deleted arm.
5. The four lint levels, and the one to avoid
allow / warn / deny / forbid, innermost wins — except `forbid`,
which cannot be relaxed by an inner `allow` (that is itself an
error). So a crate-wide `#![forbid(unsafe_code)]` is a real
promise, and a crate-wide `#![forbid(warnings)]` is a trap: the
day one generated macro trips a new lint, nobody can allow it
locally. Use deny for style, forbid for safety properties.
See also¶
- Comments that compile —
///and//!, which are attributes in disguise - What a warning is asking — the diagnostics
#[allow]silences, and when silencing is the wrong fix - Strict clippy — a crate-level lint policy, and living with it
CopyvsClone— two of the derives above, and why one is a markerconstandstatic— the other item-level metadata- Marker traits — what
derive(Eq)actually generates, which is nothing
Sources¶
Attributes ↗ in Rust by Example; the Reference's attributes ↗ chapter for the full list, and derivable traits ↗ in The Book. Both error transcripts were produced by compiling the broken version.
Po polsku¶
Atrybut (attribute) to metadane, na które reaguje kompilator — a nie kod wykonywany w czasie działania programu. Cała składnia sprowadza się do jednego wykrzyknika: #[…] dotyczy elementu pod sobą, a #![…] dotyczy tego, w czym samo siedzi (pliku albo bloku), i musi stać na samym jego początku. Warto od razu wiedzieć, że komentarz dokumentacyjny też jest atrybutem: /// to lukier składniowy na #[doc = "…"], a //! na #![doc = "…"]. Stąd bierze się typowe zaskoczenie — źle postawiony /// daje błąd o atrybucie, a nie o komentarzu.
Polskiemu czytelnikowi atrybut kojarzy się zwykle z dekoratorem z Pythona albo adnotacją z Javy, i to skojarzenie jest trafne tylko w połowie. Dekorator to funkcja wywoływana w czasie działania, adnotacja Javy to metadane czytane refleksyjnie — a atrybut Rusta potrafi usunąć element z programu. #[cfg(test)] nie znaczy „pomiń przy uruchomieniu”, tylko „tego modułu nie ma w zwykłej kompilacji”, i dlatego kod testowy nie kosztuje binarki wydaniowej ani bajta. Rozróżnienie do zapamiętania brzmi: #[cfg] usuwa, cfg!() wybiera — to drugie jest wyrażeniem zamienianym na literał true albo false, więc obie gałęzie i tak muszą przejść kontrolę typów.
Najdroższa pułapka tej strony siedzi w #[derive(Ord)]: porównanie generowane przez derive jest leksykograficzne w kolejności deklaracji pól. Ta sama struktura z polami name, score sortuje trzy rekordy do ["Ada", "Ben", "Cara"], a z polami score, name — do ["Cara", "Ben", "Ada"]. Przesunięcie pola o jedną linię wygląda w diffie na zmianę czysto kosmetyczną, a po cichu przestawia każdy sort, każdą mapę BTreeMap i każde binary_search po tym typie; jeśli kolejność jest częścią kontraktu, napisz impl Ord ręcznie, żeby zmianę widać było tam, gdzie naprawdę zachodzi. Kto zna Pythona, zna to z @dataclass(order=True) — identyczna pułapka i identyczne lekarstwo. Dwie mniejsze reguły z tej samej rodziny: #[derive(Default)] na wyliczeniu wymaga wariantu oznaczonego #[default] (E0665), a każde derive dokłada ograniczenie na typy generyczne, więc #[derive(Clone)] na Wrapper<T> daje impl<T: Clone> Clone for Wrapper<T> — i stąd Wrapper<NotClone> odmawia klonowania z powodu, którego nie ma w twoim kodzie.
Rodzina lintów ma cztery poziomy — allow, warn, deny, forbid — które zagnieżdżają się i wygrywa najbardziej wewnętrzny, z jednym wyjątkiem: forbid nie daje się poluzować, bo wewnętrzne allow jest samo w sobie błędem. Dlatego #![forbid(unsafe_code)] w korzeniu crate'a to obietnica warta złożenia, a #![forbid(warnings)] to pułapka: w dniu, w którym jedno wygenerowane makro potrąci nowy lint, nikt nie wyciszy go lokalnie. Zasada jest krótka — deny do stylu, forbid do własności bezpieczeństwa.
Szukaj po polsku: atrybuty w Ruscie · adnotacje a atrybuty · rust derive Ord field order · rust cfg attribute vs cfg macro · rust deny vs forbid lint