const and static¶
Level: 201 · working knowledge
One line: A const is a value substituted at every use; a static is one object at one address for the whole program — and that difference, not mutability, is what decides between them.
const MAX_SCORE: u8 = 5;
static METHOD: &str = "STAR";
fn main() {
println!("{MAX_SCORE} {METHOD}"); // 5 STAR
}
Both are compile-time values, both need an explicit type — there is no inference at item level — and both are SCREAMING_SNAKE_CASE by convention, with a rustc warning if they are not.
The difference, measured¶
A static is a single object at a fixed address, guaranteed, and &METHOD has type &'static str because of it. A const has no address to take: &MAX_SCORE const-promotes the value into an anonymous static, and the compiler is free to share one or emit several. So comparing &MAX_SCORE with &MAX_SCORE happens to print true on this build, and building anything on that is a mistake.
Where the substitution is visible:
const SCALE: [u8; 6] = [0, 1, 2, 3, 4, 5];
fn main() {
let a = Box::new(SCALE);
let b = Box::new(SCALE);
println!("{}", std::ptr::eq(&*a, &*b)); // false — two copies
}
The const was copied into each Box, because that is what "substituted at every use" means.
Which to reach for¶
| Use | |
|---|---|
| a limit, a scale, a tuning knob | const — the default |
| a large table you do not want duplicated at every use site | static |
| an FFI symbol, or anything whose identity is part of its meaning | static |
| shared mutable state | static holding a Mutex, OnceLock or atomic |
A const array of 10,000 entries named at three call sites is 30,000 entries in the binary. That is the one case where the default is wrong, and the fix is one keyword.
const fn¶
const fn quorum(voters: u32) -> u32 {
let half = voters / 2;
if half == 0 { 1 } else { half + 1 }
}
const QUORUM: u32 = quorum(450);
A const fn may branch, loop, index and do arithmetic. It may not allocate, call a non-const function, or read a static. Calling a plain function in a const context is E0015, "cannot call non-const function f in constants".
Marking a function const is a promise about what it does not do, and it is part of your public API — un-const-ing one later is a breaking change for anyone who used it in a const context.
Associated consts¶
Namespaced under the type, so two types can each have a MAX_SEATS and neither needs to be called MAX_SEATS_FOR_ELECTION. This is how u8::MAX and f64::EPSILON are written, and it is where most consts in real code belong.
The trap: static mut¶
It exists, it is mutable global state, and in the 2024 edition even &COUNT inside an unsafe block is refused — "creating a shared reference to mutable static", from the deny-by-default static_mut_refs lint, with &raw const COUNT offered as the replacement.
That is the language telling you the answer is a different type. A static holding a Mutex ↗, a OnceLock ↗ or an AtomicU32 ↗ is safe, needs no unsafe, and says what the sharing rule is.
If you are coming from another language¶
- Python. Module-level
MAX_SCORE = 5is astaticin the address sense — one object, referenced from everywhere — and Python has no equivalent ofconstsubstitution at all, because everything is a reference. Two habits transfer badly. Python's module constants are mutable (nothing stopsmodule.MAX_SCORE = 9), where Rust's are enforced; and Python's mutable-default and shared-global bugs are exactly whatstatic mutwould let you write, which is why Rust makes you spell them with aMutexinstead.functools.cacheon a pure function is the nearest thing toconst fnevaluation, and it happens at run time. - ABAP.
CONSTANTS lc_max_score TYPE i VALUE 5.isconst, down to the naming convention and the compile-time value, and it has the same substitution semantics — the value is baked in where it is used.CLASS-DATAis closer tostatic: one instance shared by every use, for the life of the session, and mutable — which is precisely the shared mutable global that Rust refuses to make easy. The ABAP habit worth keeping is thatCONSTANTSinside a class or interface is namespaced by it (zcl_election=>c_max_seats), which is exactly Rust's associated const and for the same reason. And ABAP has noconst fn: a constant must be a literal, so the compile-time arithmetic Rust allows has no counterpart at all. - C / C++.
#defineis the substitution half ofconstandstatic const intis the address half, so Rust's split is the one C++ programmers already carry — with the difference thatconsthere is typed and scoped rather than a preprocessor string.constexprisconst fn, arriving at the same restrictions from the same direction. - Java / C#.
static finalandconst/static readonly. C#'s split is nearly identical to Rust's and for the same reason:constis inlined into every calling assembly (which is why changing one is a breaking change there too), andstatic readonlyis a single field read at run time.
The verified output¶
Verified output of const_and_static.rs — regenerated by tools/run_examples.py, never hand-typed.
1. Both are compile-time values, and neither may be `let`
MAX_SCORE = 5, METHOD = STAR, SEATS = 4
Both need an explicit type — there is no inference at item level.
Both are SCREAMING_SNAKE_CASE by convention, and rustc warns if
they are not.
2. The difference is whether it HAS an address
&METHOD == &METHOD (static): true
&MAX_SCORE == &MAX_SCORE (const): true
Both true, and only the first one is a promise. A `static` IS a
single object at a fixed address, guaranteed. A `const` is a value
substituted at each use, and `&MAX_SCORE` has no address to take —
so rustc CONST-PROMOTES it into an anonymous static, and is free
to share one or make several. Do not build anything on that `true`.
Where the substitution is visible is a const that is not promoted:
Box::new(MAX_SCORE) twice, same address: false
Two separate values, because the const was copied into each.
3. Which to reach for
const a value with a name: limits, scales, tuning knobs. The
default, and what you want almost every time.
static when the address matters: a large table you do not want
copied into every use site, an FFI symbol, or anything
whose identity is part of its meaning.
A const of a big array is duplicated at each use; a static is not.
4. `const fn`, and what it may not do
seats_for(450) = 4 — evaluated at compile time, so it can
initialise a const. A const fn may branch, loop and do arithmetic,
and may not allocate, call a non-const fn, or read a static.
It is still an ordinary function at run time: seats_for(50) = 1
5. Associated consts, and `static mut`
Ballot::SCALE = 0-5
Ballot::describe() = STAR on a 0-5 scale
`static mut` exists and is mutable global state: every access is
`unsafe`, and in the 2024 edition even `&COUNT` inside an unsafe
block is refused — "creating a shared reference to mutable
static", from the deny-by-default `static_mut_refs` lint, with
`&raw const COUNT` offered as the replacement. What you want
instead is a `static` holding a Mutex, a OnceLock or an atomic —
safe, because those are the types that make sharing sound.
Practice¶
The address you may not rely on. Take a const array and a static array, take two references to each, and compare the pointers. Both comparisons come out the same way; only one of the two answers is a guarantee, so say which and why. Then Box the const twice and compare again, and explain the different result in one sentence.
Then two questions about const fn. Write one that computes a quorum, use it to initialise a const, and then add a version that returns a String — the second will not compile in a const context, and the error names the reason. And say why marking a function const is a semver decision rather than an optimisation.
Solution
const_and_static_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: the address you may not rely on, and the const fn that will not.
//!
//! rustc --edition 2024 const_and_static_kata.rs -o /tmp/csk && /tmp/csk
const SCALE: [u8; 6] = [0, 1, 2, 3, 4, 5];
static METHODS: [&str; 3] = ["STAR", "Approval", "Plurality"];
const fn quorum(voters: u32) -> u32 {
// Allowed in a const fn: arithmetic, comparison, if, loops, indexing.
let half = voters / 2;
if half == 0 { 1 } else { half + 1 }
}
/// Not const: it allocates.
fn quorum_message(voters: u32) -> String {
format!("{} of {voters}", quorum(voters))
}
struct Election;
impl Election {
const DEFAULT_SEATS: u32 = 1;
const MAX_SEATS: u32 = 10;
fn seats(requested: u32) -> u32 {
requested.clamp(Self::DEFAULT_SEATS, Self::MAX_SEATS)
}
}
fn main() {
println!("1. Where they may appear, and where they may not");
println!(" const QUORUM: u32 = quorum(450); -> {}", quorum(450));
println!(" const N: u32 = some_fn(); -> E0015 unless some_fn is const");
println!(" static as an array length: [u8; SCALE.len()] is fine, because");
println!(" SCALE is a const. A `static`'s value cannot be used that way.");
let sized: [u8; SCALE.len()] = SCALE;
println!(" [u8; SCALE.len()] built: {sized:?}");
println!();
println!("2. The address question, measured");
let c1: *const u8 = &SCALE[0];
let c2: *const u8 = &SCALE[0];
let s1: *const &str = &METHODS[0];
let s2: *const &str = &METHODS[0];
println!(" &SCALE[0] twice (const): {}", c1 == c2);
println!(" &METHODS[0] twice (static): {}", s1 == s2);
println!(" Both true, and only the second is guaranteed. Taking a reference");
println!(" to a const promotes the value into an anonymous static, and rustc");
println!(" may share one or emit several — so equality here is an");
println!(" observation about this build, not a rule. The static is a rule.");
let a = Box::new(SCALE);
let b = Box::new(SCALE);
println!(" Box::new(SCALE) twice, same address: {}",
std::ptr::eq(&*a as *const _, &*b as *const _));
println!(" Two copies, because the const was substituted into each Box.");
println!();
println!("3. What a const fn may not do");
println!(" quorum(450) = {} compile time or run time, either", quorum(450));
println!(" quorum_message(450) = {} run time only", quorum_message(450));
println!(" The second allocates a String, and allocation is not allowed in a");
println!(" const context. Neither is calling a non-const fn, or reading a");
println!(" `static`. Marking a function `const` is a PROMISE about what it");
println!(" does not do, and it is part of your public API: un-consting one");
println!(" later is a breaking change.");
println!();
println!("4. Associated consts, which is where most consts belong");
println!(" Election::DEFAULT_SEATS = {}", Election::DEFAULT_SEATS);
println!(" Election::seats(0) = {}", Election::seats(0));
println!(" Election::seats(50) = {}", Election::seats(50));
println!(" Namespaced under the type, so two types can each have a MAX_SEATS");
println!(" and neither has to be MAX_SEATS_FOR_ELECTION. A trait can declare");
println!(" one too, which is how `u8::MAX` and `f64::EPSILON` are written.");
println!();
println!("5. The rule");
println!(" const unless you can say why the address matters.");
println!(" static for a large table, an FFI symbol, or shared mutable state");
println!(" behind a Mutex / OnceLock / atomic.");
println!(" A const array of 10_000 entries copied into three call sites is");
println!(" 30_000 entries in the binary. That is the one case where the");
println!(" default is wrong, and the fix is one keyword.");
}
Verified output of const_and_static_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
1. Where they may appear, and where they may not
const QUORUM: u32 = quorum(450); -> 226
const N: u32 = some_fn(); -> E0015 unless some_fn is const
static as an array length: [u8; SCALE.len()] is fine, because
SCALE is a const. A `static`'s value cannot be used that way.
[u8; SCALE.len()] built: [0, 1, 2, 3, 4, 5]
2. The address question, measured
&SCALE[0] twice (const): true
&METHODS[0] twice (static): true
Both true, and only the second is guaranteed. Taking a reference
to a const promotes the value into an anonymous static, and rustc
may share one or emit several — so equality here is an
observation about this build, not a rule. The static is a rule.
Box::new(SCALE) twice, same address: false
Two copies, because the const was substituted into each Box.
3. What a const fn may not do
quorum(450) = 226 compile time or run time, either
quorum_message(450) = 226 of 450 run time only
The second allocates a String, and allocation is not allowed in a
const context. Neither is calling a non-const fn, or reading a
`static`. Marking a function `const` is a PROMISE about what it
does not do, and it is part of your public API: un-consting one
later is a breaking change.
4. Associated consts, which is where most consts belong
Election::DEFAULT_SEATS = 1
Election::seats(0) = 1
Election::seats(50) = 10
Namespaced under the type, so two types can each have a MAX_SEATS
and neither has to be MAX_SEATS_FOR_ELECTION. A trait can declare
one too, which is how `u8::MAX` and `f64::EPSILON` are written.
5. The rule
const unless you can say why the address matters.
static for a large table, an FFI symbol, or shared mutable state
behind a Mutex / OnceLock / atomic.
A const array of 10_000 entries copied into three call sites is
30_000 entries in the binary. That is the one case where the
default is wrong, and the fix is one keyword.
See also¶
- Modules and visibility —
pub constand where it is visible from &'static str— the other'static, and the confusion between them- What an attribute is —
#[allow], and the other item-level metadata - Sharing across threads:
Arc— where astaticholding aMutexends up - What a type annotation does — why both of these need their type written out, where a
letdoes not
Sources¶
constants ↗ in Rust by Example; the Reference's constant items ↗ and static items ↗, plus the edition guide's static mut references ↗, which rustc's own note links to.
Po polsku¶
Po polsku jedno i drugie nazwie się „stałą globalną” i właśnie ta nazwa myli, bo sugeruje, że różnica dotyczy niezmienności. Nie dotyczy: const to wartość podstawiana w każdym miejscu użycia, a static to jeden obiekt pod jednym adresem przez cały czas działania programu. Pytanie rozstrzygające brzmi więc nie „czy to się zmienia”, tylko „czy adres ma znaczenie”. Formalności są wspólne — oba wymagają jawnie wypisanego typu, bo na poziomie elementu nie ma wnioskowania typów, i oba zapisuje się wersalikami w stylu SCREAMING_SNAKE_CASE, bo inaczej rustc ostrzega.
Różnicę widać dopiero, gdy się ją zmierzy. static ma adres — dlatego &METHOD jest typu &'static str. const adresu nie ma, więc &MAX_SCORE powoduje promocję wartości (const promotion) do anonimowego statica, a kompilator może wyemitować jeden taki obiekt albo kilka. Porównanie &MAX_SCORE z &MAX_SCORE wychodzi w tej kompilacji true i nie wolno na tym niczego opierać; podstawianie ujawnia się dopiero przy dwukrotnym Box::new(SCALE), gdzie std::ptr::eq daje false — dwie kopie, bo dokładnie to znaczy „podstawiana przy każdym użyciu”. Stąd praktyczna reguła: const domyślnie, static wtedy, gdy adres jest częścią znaczenia (symbol FFI) albo gdy kopiowanie boli — stała tablica 10 000 elementów wymieniona w trzech miejscach to 30 000 elementów w gotowej binarce.
const fn jest obietnicą, a nie optymalizacją. Wolno w niej rozgałęziać, robić pętle, indeksować i liczyć; nie wolno alokować, wywołać funkcji nie-const ani czytać statica — próba kończy się błędem E0015, cannot call non-const function. Ponieważ ktoś mógł użyć takiej funkcji do zainicjowania stałej, późniejsze odebranie jej const psuje cudzy kod i jest zmianą łamiącą API, więc traktuj to słowo jak część publicznego interfejsu. Osobno warto zapamiętać, że static mut jest w praktyce zamknięte: w edycji 2024 nawet &COUNT wewnątrz bloku unsafe odrzuca lint static_mut_refs (domyślnie deny), a kompilator podpowiada &raw const COUNT. Odpowiedzią na zmienny stan globalny — zwłaszcza dzielony między wątkami — jest static trzymający Mutex, OnceLock albo typ atomowy: bez unsafe i z regułą współdzielenia wypisaną wprost w typie.
Szukaj po polsku: stałe globalne w Ruscie · stała a zmienna statyczna · rust const vs static · rust const fn E0015 · rust static_mut_refs 2024