How to print address of an object in rust

struct Foo{}
let z = Foo{};
println!("z addr {:p}",z);

How can I print address of z here . below code is giving me error

error[E0277]: the trait bound `Foo: Pointer` is not satisfied
   --> src/main.rs:47:24
    |
47  | println!("z addr {:p}",z);
    |                        ^ the trait `Pointer` is not implemented for `Foo`
    |
1 Like
struct Foo{}
let z = Foo{};
println!("z addr {:p}",&z);
5 Likes

Note that Rust is different from other programming languages where each “object” is some heap-allocated thing which inherently has some kind of address that doesn’t change. Instead, values in Rust are passed-around (aka “moved”) by value and moving a value like that naturally changes the address of where it’s located.

In the example code, adapted by @sfackler, the struct z = Foo {} is just a value, without any heap data or particularly stable address to it. As the code stands, it would live on the stack, and the println!("z addr {:p}",&z) just prints the location of that spot on the stack. But also, since it’s not only a value, but also a value without any data (aka “zero-sized type”, ZST), this pointer doesn’t uniquely identify the value nor the variable z. For example,

struct Foo {}
let z1 = Foo {};
let z2 = Foo {};
println!("z1 addr {:p}", &z1);
println!("z2 addr {:p}", &z2);

(playground) built in “Release” mode on Rust 1.62

prints the same address twice.

On the other hand, e.g.

struct Foo {}
let z1 = Foo {};
println!("z1 addr {:p}", &z1);
let z2 = z1;
println!("z2 addr {:p}", &z2);

(playground) built in “Debug” mode on Rust 1.62

prints two different addresses, even though it’s the same value, but because the value was moved, its location/address can have changed.

I’m specifying optimization level and compiler version, because how exactly different addresses relate to each other is something that’s allowed to change between different compiler versions or optimization levels.

Also, in many cases, if you don’t ask for an address, a value, even a non-ZST one, might not have an address at all. By writing &z and printing the address, you’ll force the compiler to make up some stack location for that value, or perhaps even put it there, but otherwise, small and short-lived values like that might as well live only ever in registers, and would thus never even be placed on RAM (except in case the scheduler would preempt the thread at that very time the value exists in a register, since such a context switch between threads of processes will involve saving all the registers).

17 Likes

This is the exact answer I was searching for. Thank you for such a detailed answer. now I have a doubt on what exactly is a mov in Rust. is it copying a value from one location of stack to another ? If so, then what exactly is the difference between mov and Copy/Clone

Not "of stack". Just from one location to another, wherever they are.

  • move vs Copy: the same runtime behavior (before optimizations), but moved value cannot be used anymore, and any attempt ot do this will be compiler error.
  • move vs Clone: the latter can be arbitrarily complex (including memory allocation), but is always explicit.

Implementation-wise duplicating a Copy type or moving a non-Copy type is the same basic operation: it’s just copying the shallow data of that value (i.e. it’s not a deep copy, everything behind pointer-indirection remains untouched).

The main difference is that non-Copy types typically cannot be duplicated, so the original value where you moved from needs to become invalidated. This is important for Rust’s ownership sytem: say…

…for example… a Box<u8> owns a heap allocation, and if it was duplicated, then there’d be two boxes owning the same heap allocation, which can’t happen (because then, dropping both of them would result in a double-free).

The compiler makes sure that always only one copy of a (non-Copy) value is considered live at each point in time; this is done mostly with static checks when you’re moving values around between variables on the stack, though it’s possible to conditionally move out of a value, and in such cases the compiler might introduce additional run-time flags on the stack to track whether or not a value is still valid, which is important to decide whether or not it needs to be dropped at the end of the scope.

You can actually take the stance that these flags as, in principle, always being involved when moving from or into values on the stack, just they’re (very) often optimized away. With these flags, the model of moving a value from, say, variable x to variable y with an assignment operation y = x consists of three steps:

  • first, if the target variable y already contains a valid value (as determined by its run-time flag) then this old value is dropped (by calling its destructor if necessary)[1]
  • then, the (shallow) data of x is copied to y
  • finally, the run-time flags for both x and y are adjusted to indicate that y contains a valid value, and x no longer contains a valid value

There’s no way to inspect these flags though, they only control destructors, and the compiler will outright prevent any other (beyond the implicit destruction at the end of its scope) usage of a variable unless it’s 100% certain (via static analysis) that at that point in the program the variable contains a valid value.

If you want to move values around with run-time checks, you can do it with Option<T>, using API such as Option::take to – essentially – de-initialize the old value, without making preventing subsequent inspection of it (since it’s new state is simply that it’s taking on a None value; serving a similar purpose as what the “drop flags” did in the previous paragraphs), and if you want to set a value you can e.g. assign Some(…value…). The example of Option::take leads naturally into a few standard-library-implemented moving primitives,

feel free to take a look. Those can move a value out from behind a &mut … reference. Since such a reference could be pointing anywhere, including on the heap, and/or in a complex data structure, there are no runtime flags anymore, and you’ll need to ensure more predictable behavior: the value can never become invalid, so you’ll need to provide an immediate replacement value (or let it use Default::default(), depending on which one of those functions you use). The fact that drop flags aren’t involved also simplifies these operations: E.g. for mem::swap the swapping really is just nothing more beyond copying/swapping the data between the two values.


The concept of “moving” in Rust is not just about moving with language-built-in operations such as assignment, or with the functions above; any custom (low level, unsafe-code involving) library can do what’s logically a “move” just by copying data behind pointers. Just gotta make sure you don’t ever accidentally duplicate any value this way. And even these standard library methods do it this way; take a look at the implementation of mem::replace, it’s really quite straightforward code.

use std::ptr;

pub fn replace<T>(dest: &mut T, src: T) -> T {
    // SAFETY: We read from `dest` but directly write `src` into it afterwards,
    // such that the old value is not duplicated. Nothing is dropped and
    // nothing here can panic.
    unsafe {
        let result = ptr::read(dest);
        ptr::write(dest, src);
        result
    }
}

The term “moving” can also refer more generally to “transferring ownership”, in which case things can “move” without actually moving in memory. So e.g. some people might consider the contents of type T to be “moving”, too, when you’re moving a Box<T>; because the (transfer of) ownership of the T comes with the (transfer of) ownership of the Box<T>.


Finally, looking back at Copy types, these types are allowed to be duplicated, so this means things like

  • the compiler’s static analysis will no longer prevent you from using a value that you did (or might have) moved from
  • you don’t need functions like mem::replace anymore to move out of a &mut T, because it’s safe to skip the part of immediately providing a replacement value; so dereferencing, say, a reference: &mut u32 into a u32 just works like let x: u32 = *reference;
  • copying is the norm for Copy types; there’s no disadvantage because it’s the same cost as ordinary moving (potentially even cheaper because you never need to deal with run-time flags for destructors)
    • this means it’s really pretty hard[2] to “actually move” a Copy type instead of copying it, but also that doesn’t matter because moving would do the same thing, just with more restrictions
    • one point however is destructors: If a Copy type had a destructor, then surely all those implicit duplications might become confusing or annoying, because for each copy, there’d be a destructor call? Right. That’s why Copy types aren’t allowed to have any custom destructors; dropping a Copy type is always a no-op.

  1. let’s, for simplicity, skip the part of what happens if this step panics; destructors typically aren’t supposed to panic anyways ↩︎

  2. or perhaps impossible, depending on what you mean by it ↩︎

6 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.