Dialogue 5001: Lessons from the gods


Lessons from the gods: Rvalue References, Move Semantics, and the Search for Meaning

The following is an annotated dialogue that is adapted from a real conversation that took place on a public IRC channel on July 4th, 2021:


Introduction: The Question that Sparked the Lesson

| simplicio | I am still wrapping my head around rvalue references, but I think I am finally starting to get it.
| salviati | rvalue references are really simple once you separate them from "move semantics"
| salviati | rvalue references are just a way to keep a mutable reference to a temporary
| salviati | const lvalue references could already keep const references to them with the same lifetime extension property
| simplicio | I just need to make sure I am clear on when I am calling a copy *constructor* versus when I am using an assignment operator.
| _________ | These are two different concepts in the Rule of Three, and yet I seem to recall I was using one when I thought I was using the other.
| salviati | rvalue references just allowed for mutability
| salviati | Move semantics are just a convention of destroying the source object in a "move constructor" (or move-assign operator)
| KittyCat | more to 'invalidate' than 'destroy', really.
| salviati | Yeah
| salviati | Because of the fact that [lvalue] references decay to [rvalue] references pretty easily, you use std::move (which is just a [wrapper]
| ________ | for a cast-to-rvalue expression) to preserve the reference type when passing them
| salviati | Actually they don't decay pretty easily --
| salviati | Oh they do in cases where you use std::forward
| simplicio | whoa wait... you are saying this so quickly but I always feel a need to slow down when I read these things....

Definition 1: "Mutable Reference"

| simplicio | "mutable reference" ... uhh mutable in what way exactly? As I recall it, rvalue references have no memory location assigned to them, but nonetheless, references in general cannot be "reassigned"
| salviati | std::move is usually used to pass an lvalue as an rvalue reference to have it invalidated as if it were a temporary
| salviati | "mutable-reference" as in "not a const-reference"
| salviati | Or rather, "reference to mutable"
| salviati | References are always constant themselves
| salviati | Or a better way to think about references is that they aren't actually values so the concept of mutability doesn't really apply to them
| salviati | int& and int&& are both "reference to mutable"
| salviati | and const int& is a "reference to const"
| salviati | I don't like that terminology because you can have "references to const" even though the referenced object isn't really const
| salviati | Just like you can have "reference to rvalue" for an object that isn't really an rvalue
| salviati | So const-ref is a reference to an object through which you cannot mutate the referenced object
| salviati | And an rvalue-ref is a mutable reference to an object, through which convention people use to implement move operations
| salviati | (there are no const rvalue references, because const lvalue references work just fine)

<digression>

| KillerWasp | salviati# the constructor and destructor in one line have a name, but i forget it
| salviati | You put your constructor and destructor in just one line?
| salviati | The only named category I can think of that contains both constructors and destructors would be "special member functions"
| KillerWasp | salviati# yes, the object is created and destroyed on the same line, which on the next line no longer exists.
| salviati | Ah, that doesn't have a name AFAIK
| salviati | That is just a temporary object which doesn't have it lifetime extended
| salviati | e.g. in the statement 1; the value is never captured so its just created and destroyed immediately
| salviati | in f(1) it's extended for the life of the call to f(int)
| salviati | and [in] int&& i = 1; it's extended for the lifetime of the name "i"

</digression>

| simplicio | "I don't like that terminology because you can have 'references to const' even though the referenced object isn't really
| _________ | const" ... can you give an example? Are you talking about some crazy kind of reference to a const pointer to a non-const
| _________ | pointer...?
| salviati | No I'm talking about the fact you can make a const reference to a non-const object
| salviati | const int& i = 123; for example
| salviati | or int i; const int& iref = i;
| salviati | You can do int i = 123; f(static_cast(i));
| salviati | f(int&&) is then called
| salviati | Which, by convention, assumes it is allowed to clobber the value of 'i'

Definition 2: "Object"

| simplicio | > Just like you can have 'reference to rvalue' for an object that isn't really an rvalue
| _________ | this confuses me even more...
| _________ | I do not consider `int` to be an object. I consider it to be a primitive type. Am I wrong?
| salviati | It is an object of primitive type
| KittyCat | I wish the language was more strict about reference/pointer-to-const
| salviati | You can also have objects of user-defined types (like structs/classes/enums)
| KillerWasp | salviati# not's Lambda?
| KittyCat | so many optimization opportunities lost basically because of const_cast and aliasing
| salviati | "object" isn't used in C and C++ to refer to high-level concept of "Object" that exists in object-oriented programming
| salviati | An object is basically something that has a value and an address
| salviati | Whether the type of the object is int, or struct { int; float; std::vector; }
| salviati | I guess its better to say an object has a type, a value, and an address
| salviati | A simple variable declaration creates both an object and a name for it
| salviati | int i = 123; -- the object is the conceptual thing that has a type of int, a value of 123, and an address you can retrieve via &i
| salviati | We call "i" a variable, but in fact its just a name referring to that object

Experiment: Memory Clobbering

| simplicio | "Which, by convention, assumes it is allowed to clobber the value of 'i'". Yes, but hold on; as long as `i` is declared as a const reference, then doesn't that imply that it's allocated in non-heap memory...? Err wait hang on, const references work weirdly, I need to check my notes...
| salviati | int&& is not a const reference, its an rvalue reference
| salviati | You can also pass a mutable object via const [reference] and you probably do it all the time
| salviati | This isn't references but: e.g. char a[100]; char b[100]; memcpy(a, b, 100);
| salviati | Here you've passed a const pointer to a mutable char
| salviati | std::string s = "Hello world"; std::string s2(s);
| salviati | Is another example
| salviati | s is a mutable object, but you've passed it via const-reference to call string::string(const string&)
| salviati | This is allowed because the rules for const-references are a subset of the rules for mutable references

| simplicio | wait I think I know why I was confused. You used the same variable as a formal parameter *and* as an actual parameter... But
| _________ | my point is, when you do `int i = 123;`, that's not allocated using the heap. So how could it be possible for the memory to be
| _________ | "clobbered" ?
| salviati | Because you can still overwrite its value
| salviati | Take this for example
| salviati | { char buf[8] = "XXXXXXX"; f(std::move(buf[0])); } f(char&& x) { x = 'Y'; }
| salviati | First note that std::move(buf[0]) is just an alias for static_cast(buf[0])
| salviati | So all we're doing is taking the natural lvalue reference you get when you name "buf[0]" and then casting it explicitly to an rvalue reference -- which is allowed!
| salviati | And then using it to call a function that takes an rvalue reference and by convention will overwrite it with a garbage value
| salviati | Now the value, which is stored only "on the stack" is now "YXXXXXX"
| simplicio | wait.... is that true? Dang... Why didn't anyone tell me.

Extended Examples

| salviati | You can do the same example with an int
| salviati | { int i = 123; f(std::move(i)); /* x is now zero */ } f(int&& x) { x = 0; }
| salviati | A more useful example is when you look at std::string...
| salviati | { std::string s = "Hello"; f(std::move(s)); /* s is now empty, and some_global holds "Hello" */ } f(std::string&& s) { std::swap(s, some_global); } std::string some_global;
| salviati | Though it's worth noting that f() here isn't even guaranteeing that 's' becomes empty
| salviati | Because by convention you assume that 's' is now invalid or in an undefined state
| salviati | Because it's an rvalue reference, the writer of f() is actually expecting you to probably call it like f("Hello") instead
| salviati | Which is going to construct a temporary std::string(), pass it by rvalue reference to f(), which is then able to efficiently move its contents in to some_global

Writing to an Rvalue

| salviati | You can also just straight up write to rvalue reference the same way as lvalue references
| salviati | { int i; int& iref = i; iref = 123; /* i is now 123 */ }
| salviati | This is not surprising
| salviati | { int&& iref = 0; iref = 123; /* the temporary object iref was referencing is now 123 */ }
| salviati | What happens in the second example is lifetime extension, and you can do the same thing with const references
| salviati | { const int& iref = 123; /* ... */ std::cout << iref; } for example is completely fine
| salviati | The lifetime of the object that is referenced here is preserved for as long as the reference is in scope
| salviati | Its the same concept that allows function calls to work
| salviati | int f(const int& i) { std::cout << i; } { f(123); }
| salviati | Here the lifetime of the object created by the expression "123" is preserved until f() returns

Conclusion & Comparison to Other Languages

| salviati | rvalue references are basically all the magic of const references, but with an allowance for mutability.
| salviati | It's easy to miss the magic when you're doing f(123); and calling f(const int&).
| salviati | Even that is something surprising that you have to learn is possible.
| salviati | You never see code in C which stores or passes a pointer to a temporary object produced by an expression
| salviati | Because you'll either get a dangling pointer or the syntax doesn't even work!
| salviati | Maybe f(&g()) is allowed. idk. It's stupid either way.
| salviati | Another way to think of rvalue references is like passing ownership of an object to you, maybe?
| salviati | If C++ was rust for example, it should definitely forbid passing the same object by rvalue reference twice
| merlin | Doesn't C++ explicitly allow that since you're required to keep the movee in a valid state?
| salviati | "the movee"?
| salviati | The thing being moved?
| merlin | Yes
| salviati | I don't think you're required to keep it in a valid state...


The conversation trails off.

Credit to the IRC user Sausage for serving the role of salviati in the above discussion.

SiegeLord played the role of merlin.

KittyCat was played by, erm... KittyCat. And KillerWasp is also famously known as rmbeer.