1

I was wondering about the following code taken from this example:

Is it possible to std::move local stack variables?

struct MyStruct
{
    int iInteger;
    string strString;
};
    
void MyFunc(vector<MyStruct>& vecStructs)
{
    MyStruct NewStruct = { 8, "Hello" };
    vecStructs.push_back(std::move(NewStruct));
}
    
int main()
{
    vector<MyStruct> vecStructs;
    MyFunc(vecStructs);
}

I understand how std::move() works on a basic level, but there's something under the hood that doesn't make sense to me.

If, for example, NewStruct was created on the stack, locally, as in the example. If I used move semantics to save its data from being destroyed before exiting the function scope, thus NewStruct being destroyed won't affect my data, which is great. But isn't this information still placed on the stack? If I were to expand my use of the stack again, why wouldn't this information be in danger of being overridden once the stack grew and wanted to write over where NewStruct data was originally created?

Adding a second example to maybe get my point across in a better manner:

    void MyFunc(vector<char*> &vecCharPointers)
    {
        char* myStr = {'H', 'e', 'l', 'l', 'o'};
        vecCharPointers.push_back(std::move(myStr));
    }

    int main()
    {
        vector<char*> vecCharPointers;
        char* cp = nullptr;
    }
crommy
  • 433
  • 1
  • 4
  • 12
  • Could you elaborate on how moving saves the data from being destroyed? – chris Jun 30 '20 at 21:58
  • Well as far as I know move simply turns NewStruct into an Rvalue and then I assume a custom move constructor creates a new object in the vector and switches ownership of that data from NewStruct to the newly created object in the vector. But isn't that data that's pointed to still on the stack in the same scope as where NewStruct was created? – crommy Jun 30 '20 at 22:00
  • `NewStruct` is local to `MyFunc()`. No matter what, `NewStruct` is going to be destroyed when `MyFunc()` exits. Other function calls after that exit will indeed overwrite the memory that `NewStruct` occupied. That is a given, and is perfectly fine, this is how local variables work. The `std::move()` lets `push_back()` create a new `MyStruct` that *steals* any **dynamic** memory the `std::string` is using for its character data, before the `std::string` is destroyed. But if the `std::string` is using an SSO buffer, there is nothing to steal, so the character data must be copied instead of moved – Remy Lebeau Jun 30 '20 at 22:12
  • Okay but what if we hypothetically had a pointer to a classical 'C' like string (char*). If we used move semantics to copy this string wouldn't we just be copying the address to the beginning of this string? And because the contents of the string is on the stack we essentially would be pointing to garbage as it would be overridden later on, no? – crommy Jun 30 '20 at 22:13
  • 1
    "*wouldn't we just be copying the address to the beginning of this string?*" - yes, but move semantics require the source be left in a "valid but undetermined" state after a move. For a C-style string pointer, a move would be to assign the source pointer to the target pointer, and then set the source pointer to `nullptr` (anything else would be invalid). "*because the contents of the string is on the stack*" - why do you think the contents are on the stack? Not everything in memory is on the stack. Case in point, a `std::string`'s internal buffer is on the heap instead (when SSO is not used). – Remy Lebeau Jun 30 '20 at 22:21
  • Okay that sounds pretty good, I made an edit though where I create a character array.. I assume it is created on the stack. Now i'll ask for forgiveness because I haven't run this code so I'm not sure if it works as I have intended. I assume that this array is created on the stack because that's what I was taught so I may be very wrong, but assuming this is on the stack, then the pointer at the end of this is left pointing to a local address which is on the stack and will be overridden later on? – crommy Jun 30 '20 at 22:29
  • "*I assume it is created on the stack*" - no, actually it is in static memory, and you have a local pointer on the stack pointing to it. An array allocated on the stack would look like this instead: `char myStr[] = {'H', 'e', 'l', 'l', 'o'};` But you can't move an array, only copy it. And moving fundamental types, such as a raw pointer, is just the same as copying it. Only move constructors and move assignment operators of a class/struct can actually move/steal things between object instances. And yes, in your example (if it even compiled), the `vector` would end up with a dangling pointer. – Remy Lebeau Jun 30 '20 at 22:57

3 Answers3

6

I think your misunderstanding lies in how your data is actually stored. The struct object will be on the stack, containing an int and a std::string object, yes. However, the actual string contents are stored on the free store, not on the stack (ignoring an optimization that isn't important here).

Now let's remove std::move and move semantics from the picture. What would happen? The vector will create a new element on the end that is copy-constructed from your stack variable. This means that the new object will have a copy of the int value and a copy of the std::string value. Copying the std::string entails allocating another block of memory on the free store and copying the data into there. In addition, any members such as size are copied as your int is. When your stack variable goes out of scope, its int and the string will be destroyed, which causes the string to clean itself up, not touching the new copy in any way. What you're left with after that is the new object at the end of the vector with its own copy of the data.

If you'll excuse the poor diagram:

copy diagram showing stack vs. free store

How does moving change this? Moving is simply an optimization to avoid unnecessary copies when possible. Copying is a valid move tactic, just not a good one if you can do better. Using std::move will ultimately cause the vector to move your stack object when creating the new object at the end of the vector. The int will still be copied because there's no optimization to be had there. However, the string can take advantage of the fact that this free store data is no longer needed. The new object can simply steal the pointer, copy the size etc., and tell the moved-from object not to clean up that free store data (transferring ownership).

If you'll excuse a slightly modified version of the original poor diagram:

enter image description here

We saved allocating a separate block for the string, but that's all. The other data was all still copied. The actual string data will be cleaned up by the vector element when it is removed or the vector is destroyed. The stack element now has nothing to clean up because moving has "stolen" the string data rather than copying it. You can think of it as nulling out the stack object's pointer, even though an implementation is free to represent an empty string differently.


What I said doesn't fully apply here because major implementations of std::string are clever enough to avoid extra allocations for small strings. That simply means the string data would need to be copied because as you say, it would die when the original object does. Any extra allocations are open to move optimizations, though.


As for your second example, there's no general optimization you can do to move a raw pointer; it's (usually) just 4 or 8 bytes to copy over. Moving that into the vector would copy the pointer value, leading to a dangling pointer within the vector once the function ends. The myStr pointer within the function would be destroyed when the function ends and not affect the vector in any way.

chris
  • 60,560
  • 13
  • 143
  • 205
  • That's quite an answer, I'm apauled it was not the accepted one, especially but not exclusively, given the artwork. – anastaciu Jun 30 '20 at 22:50
  • 2
    @anastaciu, No need to worry about such things. If the answer helps anyone, it's done its job. – chris Jun 30 '20 at 23:47
3

If NewStruct was entirely on the stack, the moving it would be no more efficient than copying it. To create a new object with the same contents as NewStruct would require copying everything from the stack to wherever the new object is.

Where move semantics are helpful are for objects that aren't entirely on the stack (or, to be more precise, don't have their entire contents stored in the object's fixed-size portion). For example, a std::string or a std::vector (or an object that contains them) will typically have a buffer that holds variable-length data that is allocated from the heap. Move semantics can transfer ownership of this buffer from the old object to the new object, saving the need to allocate a new buffer and copy the contents of the old buffer into it.

David Schwartz
  • 179,497
  • 17
  • 214
  • 278
  • Wasn't really aware of this string buffer beforehand so this is quite useful to know :) And it pretty much clarifies up everything I wanted to know but I still wrote up a second example, thinking this character array would end up on the stack... Now wouldn't we have a problem with the second example as we're technically just copying the address to an array on the stack? – crommy Jun 30 '20 at 22:32
  • @crommy In your second example, `move` has no effect. Whether you use `move` or not, you are still only saving the value of the pointer, a value that is the address of an object that no longer exists. Making a vector of raw pointers is generally considered a bad idea as something else has to manage the lifetimes of the objects and nothing does in your example -- with or without move. – David Schwartz Jun 30 '20 at 22:35
  • Yeah honestly I'm just grasping at straws at this point, trying to find an example where using move constructor / move semantics for an object which was created locally could be dangerous.. but I doubt I'll think up of anything that's clever and maybe that's just a sign that it is safe :) thanks – crommy Jun 30 '20 at 22:38
0

Move semantics have no effect on the object's scope. In this example, NewStruct has automatic scope, so it remains in scope until the end of the function it's declared in.

Moving the contents of NewStruct makes no difference, whatsoever. NewStruct's scope is still automatic, and it only gets destroyed when its function returns.

Declaring additional objects in automatic scope, in the same function, has the same practical effect as if NewStruct's contents were not moved anywhere.

"Moving" an object doesn't mean what it seems you think it means. It doesn't mean "this object vanishes in a puff of smoke, right this instant", and no longer exists, from that point on. It still does, after the move, in some valid but unspecified state. Move semantics basically means that you're telling your C++ compiler: "copy this object, but I don't care about its contents after the copy, so if this lets you do some efficient optimization, go knock yourself out". That's all. The object continues to exist, in some valid, but unspecified state, until the end of its scope.

isn't this information still placed on the stack?

Yes, it is. As an side: technically, there's no mention of "stack" anywhere in the C++ standard; a stack is just a practical implementation of automatic scope.

If I were to expand my use of the stack again, why wouldn't this information be in danger of being overridden once the stack grew

Because NewStruct is still a valid object, and it still exists until the end of the function, at which point all objects on the stack get destroyed.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148