R-Value references - A user and compiler's POV
Hi all, I was bored and wasy just playing around with godbolt and tried to analyse r-valuer eferences a little. So let’s see what they are and how we and compiler look at them. When I say we I mean the user and the C++ features set.
r-value references
what it is
- “r-value” refers to the value that is stores in some memory address, it’s not a pointer pointing to some location and doesn’t hold its own memory address like a pointer does.
- “r-value” references (T&&) allow you to bind to r-values, which are typically temporary objects or values resulting from evaluating a expression.
- They use move semantics to get refences to r-values.
- For instance, T&& makes sure we can’t bind nullptr, it’s type-safe.
What it isn’t
- “Not a pointer” they don’t store or care about memory location, they are just references formed by r-values.
- They complement l-value references and behave similar to them.
- Pointers are more versatile, allowing dynamic memory management, reassignment, and explicit control over memory.
POV of user and C++
- Designed for modern C++ features like move semantics and perfect forwarding.
- They enable resource transfers by avoiding deep copy.
- R-value references are primarily designed for resource-efficient operations, like moving temporary objects.
POV of a compiler
Consider this snippet, you can check it out at godbolt here: https://godbolt.org/z/c6M6Pv3hj
We pass a value by it’s address and by it’s r-value reference to modify the value, we use move semantics to get it’s r-value and thus modify the value referenced. Seems like what a pointer does but quite not the felxibility that pointers give us. They are quite clearly different from a high-level usage POV.
The only difference it unlike a pointer when trying ot modify it’s value it generated code that is Implicitly Dereferenced, for pointers it’s the users responsibility to explicitly do it.
void test(int&& val)
{
val = 5;
}
void testp(int* val)
{
*val = 7;
}
int main()
{
int val = 2;
test(std::move(val));
testp(&val);
return 0;
}
now the assembly for void test(int&& val)
looks like:
void test(int &&) PROC ; test
mov QWORD PTR [rsp+8], rcx
mov rax, QWORD PTR val$[rsp]
mov DWORD PTR [rax], 5
ret 0
void test(int &&) ENDP ; test
and for void testp(int* val)
looks like:
void testp(int *) PROC ; testp
mov QWORD PTR [rsp+8], rcx
mov rax, QWORD PTR val$[rsp]
mov DWORD PTR [rax], 7
ret 0
void testp(int *) ENDP ; testp
You can see how the compiler generates the same instructions. So they are implicitly derefences and treated the same under the hood, but the language makes sure it’s not a “pointer”.
Closing notes
So yeah, this is just some analysis I did to see how stuff works under the hood. Now as for life, not much is “cooking” except me 🧑🏼🍳. It’s been a “cold” few days and I’m trying to get more productive and back to basics, so expect more simple stuff from me in the coming days. The cold chilly weather is really making me loose track of time.
references
- https://www.internalpointers.com/post/understanding-meaning-lvalues-and-rvalues-c#google_vignette
- https://stackoverflow.com/questions/5346836/c11-distinguishing-rvalue-pointers