I am newbie in rust and trying to wrap my head around references. I come from C/C++ programming background and trying to understand references.
From what I understand, reference has some similarity to pointer but not entirely the same thing.
This is what confuses me the most:
fn main(){
let x = String::from("Hello"); //x is pointer to string allocated on heap
let y = &x; // y holds address of x and not the string itself.
println!("{}", y); // This prints the string "hello" and not the address of x.
}
My confusion lies in the println! statement, why does it print the content and not the address.
Isn't y pointing to address of variable x.(y does not hold the string itself).
Does borrowing in rust as simple as borrowing data?
If that's the case, why does the following piece of code yield the same result
fn main(){
let x = String::from("Hello"); //x is pointer to string allocated on heap
let y = &(*x); // y holds address of string
println!("{}", y); // This prints the string "hello" and not the address of string.
}
Your rough intuition about what the types hold is correct, but this behavior matches c++.
Consider:
#include <string>
#include <iostream>
int main() {
auto x = std::string("Hello");
auto y = x.data();
std::cout << "x: " << x << std::endl;
std::cout << "y: " << y << std::endl;
}
Just like in the rust example, both will print out the string for each value. Just like C++, Rust treats a pointer to a string as a string automatically. They are different things, but for purposes of display, they are treated the same.
Now, to be clear, a more true-to-faith equivalent c++ program would be:
#include <string>
#include <iostream>
#include <string_view>
int main() {
auto x = std::string("Hello");
auto y = std::string_view(x);
std::cout << "x: " << x << std::endl;
std::cout << "y: " << y << std::endl;
}
I used the data example because it matched your wording closely.
Display is implemented for &T when T: Display by just forwarding the method call on to T's implementation. This is typical for many traits, like PartialOrd for instance. It's rare to have to care about the address of references,[1] whereas it's common to exercise traits through references. (And after all, as you pointed out in your code comments, x doesn't hold the textual data itself either.)
If you want to print the address, you can use the Pointer format.
// vv
println!("{:p}", y);
and Rust doesn't have address-based object identity ↩︎
Good. C++ is wast enough that almost all issues that can happen in Rust have analogues in C++. If you you are surprised by something in Rust then 9 times out of 10 you just forgot about the exact same situation in C++.
uint32_t* p = new uint32_t[4]{};
p[0] = 65;
p[1] = 66;
p[2] = 67;
std::cout << p << std::endl;
uint8_t* q = new uint8_t[4]{};
q[0] = 65;
q[1] = 66;
q[2] = 67;
std::cout << q << std::endl;
output:
0x2ea652b0
ABC
Here p is printed as address while for q content of array is print… why doesn't it surprises you? One would think that different handling very similar types uint32_t* and uint8_t* should be more surprising than different handling of quite dissimilar types, isn't it?
Rust follows similar idea, it just defines things that “natural” differently. Because Rust doesn't handle “address arithmetic” on references you very rarely need to know where reference itself points to (just like in C++ it's much more common to care about content of array that uint8_t* points to then address of such array).
To be more specific, when we borrow in rust. Do we borrow its address or its content.
That's the thing where I am tripping over.
For eg. consider the same example:
let x = String::from("Hello");
let y = &x;
From reader's perspective, it appears that y borrows from x.
Does that translate to:
a) y points to string "Hello" OR
b) y points to x which in turn points to "Hello"
Here y₁ does (a) and y₂ does (b). Both are valid Rust.
But default is to do (b) (which, then, can be converted to (a)).
And reason for why it works that way is obvious: if you want to support both the default have to be (b) because going from (b) to (a) is possible but opposite transition is not possible.
let x: String = String::from("Hello");
let y: &String = &x;
let z: &str = &*x;
+---+---+---+---+---+---+---+---+
| Pointer | Length | z: &str
+---+---+---+---+---+---+---+---+
|
V
+---+---+---+---+---+---+---+---+ str data
| H | e | l | l | o | .............. (a heap allocation in this case)
+---+---+---+---+---+---+---+---+ ((with perhaps some uninit. bytes))
^
|
+---+---+---+---+---+---+---+---+---+---+---+---+
| Pointer | Length | Capacity | x: String
+---+---+---+---+---+---+---+---+---+---+---+---+
^
|
+---+---+---+---+
| Pointer | y: &String
+---+---+---+---+
When you borrow the String (y: &String), are you borrowing the str data, which is behind some raw pointer indirection? It is somewhat a question of semantics, but from a practical perspective you are borrowing the str data, as the only non-UB way to access that data is by going through the String.
When you create a reference to the str data itself...
// Via dereference
let z = &*x;
// Which is notionally the same as
let z = x.deref();
let z = <String as Deref>::deref(&x);
// Which happens implicitly via "deref coercion" due to an annotation here
let z: &str = &x;
Then x still remains borrowed for as long as z is active, because Deref::deref borrows all of &self. String does not offer a way to borrow the str data without borrowing all of the String. So the str data is borrowed,[1] and the String data -- pointer, length, and capacity -- are also borrowed.
(Sometimes "borrow splitting" is possible due to public fields, but String has no public fields.)
Side note: the only thing you can do with a &String that you can't do with a &str is check the capacity, so you usually want a &str instead of a &String (in, e.g., your function signatures) as it's less indirect and more general.[2]
less ambiguously than the &String case, semantics-wise ↩︎
str data can exist outside of a String, such as in static memory, as part of a PathBuf, etc. ↩︎