The different levels of const-ness are vital to lower levels of systems programming. The important distinction in systems programming between a compile time constant and an immutable variable relates to performance, the programmer's intent, and safety in the presence of external effects. Two prominent examples, Rust and C, make this distinction, and in more or less the same way.
Actual compile-time constants (that definitely are)
C has #define
s that can effectively be considered direct textual substitution — copy and paste, if you like. Rust's const
s are the same, except they're typed. It is useful to consider that Vec::new()
is const
!
Why is creating a dynamically growable array const
though? Because a vec does not allocate when it's created. There are no runtime side effects at this point. The data structure that makes it up can be populated with the platform's equivalent of a null pointer and zero length entirely at compile time. This leads to confusion over this kind of code:
const CONST_VEC: Vec<String> = Vec::new();
pub fn main() {
CONST_VEC.push("Hello".into());
CONST_VEC.push("world!".into());
dbg!(CONST_VEC);
}
The output of this is:
[src/main.rs:7] CONST_VEC = []
Why? Because this code effectively copy-pastes Vec::new()
into each place where you see CONST_VEC
. That's what a compile time constant is, a straight up substitution of compile-time-computable values. (You get warnings about this. Just to be clear. Rust knows you're doing something silly here.)
The point of this kind of copy-paste constant is simply to reduce repetition and document intent by naming, without compromising on the performance gains of static program parameters. Repetition of literals is bad because each repetition is the site for a potential mis-copy. Make the infallible machine do the copying! Naming is good, because identical literals may have different purposes, and a simple find-and-replace does not know that. Tell the fallible human which number to replace when a requirement changes!
Immutable variables (that might not be)
In a lot of cases, yes, there is no difference. Indeed in C++ a static const
that is not also volatile
can be treated the same as a #define
or Rust const
— a copy-paste. (Someone please correct me if I'm wrong, I'm not nearly as familiar with C++.)
But. In both Rust and C, their respective immutable variables can change, even within the lifetimes of the bindings. This is not some quirk or hackery, it is not unsafe, it is by design. The "immutability" is thus about what the programmer intends and declares, not the data itself.
In Rust, a non-mut
variable can change due to interior mutability eg. using one of the UnsafeCell
-derived types. You might regard this as a technicality or escape hatch, but interior mutability, and more importantly safe interior mutability, is a fundamental part of Rust's goal of combining usefulness and safety. UnsafeCell
might expose an unsafe API, but there are plenty of safe interior mutability wrappers in Rust's stdlib eg. RefCell
and Mutex
.
In C, a const volatile
variable is perfectly valid. What does it mean for a variable to be both const
("does not change") and volatile
("can change outside of operations known to the compiler")? One of those mental models must be wrong, and it's the first one. const
simply means that the programmer, the human typing the word const
, is declaring that they will not need to write code that directly changes it. The C compiler can enforce this. If the variable is not also volatile
, the compiler can also make decisions about optimisations (reordering, combining, even total elision). If it is volatile
, the compiler can only (but still) enforce the intent declared by the programmer.
Examples of where this commonly applies, in both languages:
- hardware exposed as a read-only memory-mapped peripheral eg. a UART receive buffer
- data crossing FFI boundaries
- data shared across thread or process boundaries.
Embedded and OS programming (for example) depend upon:
- having practical support from the language to simultaneously...
- constrain what the programmer can mutate...
- while accounting for the presence of externally driven side effects;
- achieving high performance by exploiting knowledge of program parameters
Taking a higher view then, these different levels of const
-ness exist because of the intersection of requirements that are especially common in (although not exclusive to) low-level applications.