Using const or static?

There are two forms of "constants" in Rust:

constants

Rust has two different types of constants which can be declared in any scope including global. Both require explicit type annotation:

  • const: An unchangeable value (the common case).
  • static: A possibly mutable variable with 'static lifetime. The static lifetime is inferred and does not have to be specified. Accessing or modifying a mutable static variable is unsafe.

Which one is better to use in the following case?

  • const
  • static (without mut)
const CONST_A: &'static str = "apple";
const CONST_B: &'static str = "banana";

static STATIC_A: &'static str = "apple";
static STATIC_B: &'static str = "banana";

fn open_dbs1(prefix: &str) {
    let db_a = format!("{prefix}_{CONST_A}");
    let db_b = format!("{prefix}_{CONST_B}");
    println!("Opening \"{db_a}\" and \"{db_b}\"");
}

fn open_dbs2(prefix: &str) {
    let db_a = format!("{prefix}_{STATIC_A}");
    let db_b = format!("{prefix}_{STATIC_B}");
    println!("Opening \"{db_a}\" and \"{db_b}\"");
}

fn main() {
    open_dbs1("mydb");
    open_dbs2("mydb");
}

(Playground)

Output:

Opening "mydb_apple" and "mydb_banana"
Opening "mydb_apple" and "mydb_banana"

I assume one difference is maybe the stability of the memory address? That doesn't matter to me. Yet I have two choices and don't know what's best. The reference doesn't really give me an advice which of those two options to use in which case.

Use const if you don't have a reason to use static.

6 Likes

Okay, is there a reason or explanation for it? I assume const allows for more optimizations during compilation, while static behaves more like an ordinary variable?

Edit: And const fn's cannot use statics. (Playground)

You shouldn't ask the compiler for things you aren't going to use.

A const cannot change at runtime - it's entirely a compile time artefact.

A static can be changed at runtime, via interior mutability (e.g. static COUNTER: std::atomic::AtomicU64 = std::atomic::AtomicU64::new(0) can be altered using methods like store).

By making something const, you tell the compiler that interior mutability is explicitly not wanted here, and it can both make more assumptions safely about your code, and give you errors if you do use interior mutability.

3 Likes

Essentially, yeah. The main reason to use a static variable over a const is that you need there to be exactly one instance of the value it contains.

For example, if I'm just pulling out a URL into a constant so I don't need to hand-write it every time then I would use a const. Conceptually, you can think of the compiler as copy/pasting a const variable's definition into every place it gets used.

const RUST_USER_FORUMS: &str = "https://users.rust-lang.org/";

However, if I need a global counter to count the number of memory allocations my program makes, I would use a static.

static BYTES_ALLOCATED: AtomicU64 = AtomicU64::new(0);

In this case I only want there to be one AtomicU64 that every piece of code references. If we went with a const, updating BYTES_ALLOCATED would (conceptually) look like AtomicU64::new(0).fetch_add(bytes, Ordering::SeqCst) and always return bytes (because 0 + bytes = bytes). Later on, when my program wants to print the number of bytes to the screen it would always think 0 bytes have been allocated because the post-copy/paste code looks like println!("Bytes allocated: {}", AtomicU64::new(0)).

4 Likes

Okay, thanks for all the responses! So basically:

If I get it right, then const is really a constant (evaluated at compile time and possibly re-inserted into the code wherever used) and may have multiple instances or varying memory addresses, while static is just a single "global variable" (that can be formally immutable but still have inner mutability), which gets initialized with a constant(!) or literal though:

#![allow(dead_code)]

const C: i32 = 5;
static V: i32 = C;

const CC: i32 = C;
static CV: i32 = C;

//const VC: i32 = V; // doesn't work
static VV: i32 = V;

(Playground)

Edit: Oh, not completely right, as the static can be initialized with another static? (see last line)

Also thanks for pointing me to AtomicU64 as a practical example. (Playground)


I tested a bit more how statics can be initialized:

const ZERO: i32 = 0;
static ZERO_STATIC: i32 = 0;

fn not_const() -> i32 {
    assert_eq!(ZERO, ZERO_STATIC); // can use any of these
    ZERO_STATIC
}

const fn is_const() -> i32 {
    //ZERO_STATIC // doesn't work
    ZERO
}

struct NotCopy;

#[derive(Clone, Copy)]
struct MayCopy;

static A: NotCopy = NotCopy;
//static B: NotCopy = A; // doesn't work

static C: MayCopy = MayCopy;
static D: MayCopy = C;

//static E: i32 = not_const(); // doesn't work
static F: i32 = is_const();

(Playground)

So statics can be initialized from other statics if their type is Copy. But neither consts nor statics can be initialized with non-const function calls (which is why I used lazy_static in past).

Anyway, I will use consts where I can, and statics where I need to, then.

Pretty much.

Technically the requirement is that static variables can't have destructors because there is no reliable mechanism for executing a piece of code (the variable's destructor) before a program exits, and silently leaking destructors isn't ideal (imagine storing a File writer - normally you want its destructor to flush any buffers to disk).

Almost by definition, Copy types can't have destructors, so that's why it looks like static variables must be Copy. I think your static B: NotCopy = A example fails to compile because it moves the NotCopy out of A into B, which isn't valid because then A would be uninitialized.

One trick to check whether a type implements a particular trait is with some dummy assert_copyable() function that is generic over any T: Copy.

struct NotCopy;

static VARIABLE: NotCopy = NotCopy;

#[test]
fn variable_is_not_copy() {
    fn assert_copyable<T: Copy>() {}
    
   // error[E0277]: the trait bound `NotCopy: Copy` is not satisfied 
   assert_copyable::<NotCopy>();
   //                ^^^^^^^ the trait `Copy` is not implemented for `NotCopy`
}

(playground)

Interesting side note: you can create reference cycles between static variables using both a normal initializer or a const fn constructor.

struct Cyclic {
    other: &'static Cyclic,
}

static FIRST: Cyclic = Cyclic { other: &THIRD };
static SECOND: Cyclic = Cyclic::new(&FIRST);
static THIRD: Cyclic = Cyclic::new(&SECOND);

impl Cyclic {
    const fn new(other: &'static Cyclic) -> Self {
        Cyclic { other }
    }
}

(playground)

When people are first shown &T and &mut T, we normally call them "immutable" and "mutable" references. As you get more familiar with the language, a more complete way to phrase it is as "shared" and "unique" references.

Using this phrasing, your static variable is shared and the only way to modify it is via synchronisation/interior mutability.

2 Likes
  • static types can be non-Copy and have destructors, but the destructor won't be ran.
  • static types can be initialized from non-Copy consts.
  • static types can refer to other statics.

But you can't move out of a static, e.g.

static s1: String = String::new(); 
static s5: String = s1;

Playground.

7 Likes

Very interesting. So I'm still limited with how I can initialize a static though (I can't run arbitrary functions but only const fns), but the reason for requiring Copy when initializing it from another static is that I can't move it out from the original static.

So here const allows more:

struct NonCopy;

const A: NonCopy = NonCopy;
const B: NonCopy = A;

static C: NonCopy = NonCopy;
//static D: NonCopy = C; // won't work

(Playground)

And types used for static (as well as for const) may have destructors indeed:

#![allow(dead_code)]

struct HasDestructor {
    msg: &'static str,
}

impl Drop for HasDestructor {
    fn drop(&mut self) {
        println!("DESTROY: {}", self.msg);
    }
}

const A: HasDestructor = HasDestructor { msg: "constant" };
static B: HasDestructor = HasDestructor { msg: "static var" };

fn main() {
    let x = HasDestructor { msg: "Hello World!" };
    drop(x);
    // try these:
    //let y = A; // works
    //let y = B; // won't work
}

(Playground)

Here, static B may have a destructor. The example compiles (with the last line commented out). The problem with the last line is that it can't move out of B and not because it has a destructor.

In contrast, when I write let y = A, it doesn't move out: instead it just creates a new instance of HasDestructor (which later gets destroyed). Writing let y = A; several times will also run the destructor several times (later).

It was never about mutability. const is closer to a literal, while static is a global.

Not really. Both consts and statics have to be initialized with a value known at compile time, so I see no reason why one could be better optimized than the other. Even if for some technical circumstance, this would be the case, the difference is likely completely negligible.

Of course there is overlap between the functionality of the two constructs, and in many cases, you could use either. However, there are still many situations where it makes sense to use one but the other:

  • If you simply want to give a name to a particular value, then you should use a const. There is no reason to require that it be realized once and exactly once in memory. If you don't use it, why put it there at all, and if you do use it multiple times, it's usually fine to let the compiler duplicate it.
  • const can be an associated item, but static can't. For this reason, if you want to associate a value with a trait, you have no choice but to use const.
  • static can store mutable state (check out once_cell), but others have already elaborated on this.
3 Likes

It’s probably worth noting that there is a Clippy lint (large_const_arrays, defaults to warn) for cases where using a const could result in inefficient memory usage.

1 Like