7

The documentation for const:

Constants live for the entire lifetime of a program. More specifically, constants in Rust have no fixed address in memory. This is because they’re effectively inlined to each place that they’re used. References to the same constant are not necessarily guaranteed to refer to the same memory address for this reason.

I've only seen "inline functions" in C++, but never inline constant values. What is a beginner friendly explanation of how this works?

I'm also confused by "no fixed address in memory". Does that mean every time we use a const value, a value on the stack is allocated just for this expression and after the expression is done executing, it'll be destroyed?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366

3 Answers3

8

I've only seen "inline functions" in C++, but never inline constant values.

The closest approximate to a const in Rust is an enum in C++.

What is a beginner friendly explanation of how this works?

The simple beginner's explanation is: it just works, don't worry about the nitty-gritty details.

I'm also confused by "no fixed address in memory". Does that mean every time we use a const value, a value on the stack is allocated just for this expression and after the expression is done executing, it'll be destroyed?

Yes. Maybe. No.

It means exactly what it says on the tin: no guarantee is made. This leaves the compiler with the maximum freedom to optimize things.


Alright, that's all good and well, but... what really happens?

In practice, there are two situations:

  • the value is simple enough: it does not even touch the stack, and instead is hardcoded directly in the assembly. This is most likely to happen for integers, for example.
  • the value is not that simple: it is created in read-only memory, and referenced/copied from there. Multiple copies on the stack will have different addresses.

What does simple mean? Well, it depends. For each call site the compiler may decide "simple enough" or not, which is where it is close to inlining.

Does that mean every time we use a const value, a value on the stack is allocated just for this expression and after the expression is done executing, it'll be destroyed?

It will not be destroyed. const variables cannot have a type that implements Drop. The value is just forgotten when it is no longer used. If it ever occupied memory on the stack, this memory will be overwritten sometime later.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • Can you explain why C++ enum is the closest thing? – Pavel Strakhov Oct 20 '16 at 11:41
  • 2
    @PavelStrakhov: an `enum` value in C++ is a named value with no memory address (if you have `enum { X };` then `&X` will raise a compile-time error), and the value of the enum is inlined at each and every site of use. It's a bit closer than a `#define` because it's only computed once (and it's also much cleaner: respects scopes, etc...). – Matthieu M. Oct 20 '16 at 11:53
  • 2
    You make it sound like the compiler may decide to convert it into a `static` to avoid copies, but is that really the case? I don't seem to be able to get the compiler to do it; it always produces code equivalent to inlining the expression, even when it's obviously suboptimal. – Veedrac Oct 20 '16 at 12:04
  • @Veedrac: I thought rustc behaved like Clang in this regard, but my memories may be playing tricks on me here, especially since I thought way too much about `const fn` and what values may ultimately be stored in `const` (like `String`, `HashMap` but not `Arc` or `RefCell` because interior mutability prevents storage in ROM). I suppose that's one more instance of the work being pushed down to LLVM then :x – Matthieu M. Oct 20 '16 at 12:25
  • 2
    @Veedrac: I checked a few variations on the playground and indeed rustc emits the definition inline in IR. Of course, in Release, LLVM just constant-folds it and you end up with the integer directly in a register. – Matthieu M. Oct 20 '16 at 13:00
5

const N: i32 = 5 in Rust is like #define N 5 in C or C++ done with type-safety.

You can think of it like a textual substitution when the type matches, that is, let foo = 32 + N; is equivalent to let foo = 32 + 5; in your example.

WiSaGaN
  • 46,887
  • 10
  • 54
  • 88
1

A constant doesn't behave like a regular variable at all; when you define one it doesn't even get its own scope for borrow-checking purposes.

Compare the MIR generated by the following pieces of code:

fn main() {
    const N: i32 = 5;
}

and

fn main() {
    let n: i32 = 5;
}

And you will find that what N expands to looks more like a function than a variable:

const main::N: i32 = {
    let mut _0: i32;                     // return pointer

    bb0: {
        _0 = const 5i32;                 // scope 0 at <anon>:2:20: 2:21
        return;                          // scope 0 at <anon>:2:5: 2:22
    }
}

When it is used in an expression, its value is placed on the stack for the purpose of that expression and is forgotten right afterwards.

Edit: at least this is what happens on MIR level. I'm not an expert on low-level optimizations, so the actual outcome (whether stack memory was actually used) would have to be checked in LLVM or even ASM. Note that this is also applicable to regular variables, though.

ljedrz
  • 20,316
  • 4
  • 69
  • 97