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

22,723

Solution 1

First, std::move does not move, and std::forward does not forward.

std::move is a cast to an rvalue reference. By convention, rvalue references are treated as "references you are permitted to move the data out of, as the caller promises they really don't need that data anymore".

On the other side of the fence, rvalue references implicitly bind to the return value of std::move (and sometimes forward), to temporary objects, in certain cases when returning a local from a function, and when using a member of a temporary or a moved-from object.

What happens within the function taking an rvalue reference is not magic. It cannot claim the storage directly within the object in question. It can, however, tear out its guts; it has permission (by convention) to mess with its arguments internal state if it can do the operation faster that way.

Now, C++ will automatically write some move constructors for you.

struct MyStruct
{
  int iInteger;
  string strString;
};

In this case, it will write something that roughly looks like this:

MyStruct::MyStruct( MyStruct&& other ) noexcept(true) :
  iInteger( std::move(other.iInteger) ),
  strString( std::move(other.strString) )
{}

Ie, it will do an element-wise move construct.

When you move an integer, nothing interesting happens. There isn't any benefit to messing with the source integer's state.

When you move a std::string, we get some efficiencies. The C++ standard describes what happens when you move from one std::string to another. Basically, if the source std::string is using the heap, the heap storage is transferred to the destination std::string.

This is a general pattern of C++ containers; when you move from them, they steal the "heap allocated" storage of the source container and reuse it in the destination.

Note that the source std::string remains a std::string, just one that has its "guts torn out". Most container like things are left empty, I don't recall if std::string makes that guarantee (it might not due to SBO), and it isn't important right now.

In short, when you move from something, its memory is not "reused", but memory it owns can be reused.

In your case, MyStruct has a std::string which can use heap allocated memory. This heap allocated memory can be moved into the MyStruct stored in the std::vector.

Going a bit further down the rabbit hole, "Hello" is likely to be so short that SBO occurs (small buffer optimization), and the std::string doesn't use the heap at all. For this particular case, there may be next to no performance improvement due to moveing.

Solution 2

Your example can be reduced to:

vector<string> vec;
string str; // populate with a really long string
vec.push_back(std::move(str));

This still raises the question, "Is it possible to move local stack variables." It just removes some extraneous code to make it easier to understand.

The answer is yes. Code like the above can benefit from std::move because std::string--at least if the content is large enough--stores it actual data on the heap, even if the variable is on the stack.

If you do not use std::move(), you can expect code like the above to copy the content of str, which could be arbitrarily large. If you do use std::move(), only the direct members of the string will be copied (move does not need to "zero out" the old locations), and the data will be used without modification or copying.

It's basically the difference between this:

char* str; // populate with a really long string
char* other = new char[strlen(str)+1];
strcpy(other, str);

vs

char* str; // populate with a really long string
char* other = str;

In both cases, the variables are on the stack. But the data is not.

If you have a case where truly all the data is on the stack, such as a std::string with the "small string optimization" in effect, or a struct containing integers, then std::move() will buy you nothing.

Share:
22,723
Allgaeuer
Author by

Allgaeuer

Updated on July 09, 2022

Comments

  • Allgaeuer
    Allgaeuer almost 2 years

    Please consider the following code:

    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);
    }
    

    Why does this work?

    At the moment when MyFunc is called, the return address should be placed on the stack of the current thread. Now create the NewStruct object gets created, which should be placed on the stack as well. With std::move, I tell the compiler, that i do not plan to use the NewStruct reference anymore. He can steal the memory. (The push_back function is the one with the move semantics.)

    But when the function returns and NewStruct falls out of scope. Even if the compiler would not remove the memory, occupied by the originally existing structure from the stack, he has at least to remove the previously stored return address.

    This would lead to a fragmented stack and future allocations would overwrite the "moved" Memory.

    Can someone explain this to me, please?


    EDIT: First of all: Thank you very much for your answers. But from what i have learned, I still cannot understand, why the following does not work like I expect it to work:

    struct MyStruct
    {
        int iInteger;
        string strString;
        string strString2;
    };
    
    void MyFunc(vector<MyStruct>& vecStructs)
    {
        MyStruct oNewStruct = { 8, "Hello", "Definetly more than 16 characters" };
        vecStructs.push_back(std::move(oNewStruct));
    
        // At this point, oNewStruct.String2 should be "", because its memory was stolen.
        // But only when I explicitly create a move-constructor in the form which was
        // stated by Yakk, it is really that case.
    }
    
    void main()
    {
        vector<MyStruct> vecStructs;
        MyFunc(vecStructs);
    }
    
  • Yakk - Adam Nevraumont
    Yakk - Adam Nevraumont about 7 years
    @FrançoisAndrieux Which seems reasonable; with SBO, that permits them to be lazy and not do extra work when moving instead of copying. I'm not about to confirm with a standard delve, because it doesn't matter. :)
  • Matthew Woo
    Matthew Woo over 5 years
    So if I understand correctly, while std::move can be used with stack variables, its benefit lies in types that use heap memory. Moving an array that's on the stack won't do anything, nor a struct constituted of primitive types. Is this correct?
  • Yakk - Adam Nevraumont
    Yakk - Adam Nevraumont over 5 years
    @matt Move on primitive types does a copy. Move on aggregate of primitive types (array or struct) does a copy. There are however very few observable effects of having two copies of primitive data; so compilers can elides their existence often using the as-if rule. The most common issue is they must have distinct addresses, thus blocking elision; but, addresses of out of scope locals are undefined. Move on std library cpntsoners (except array) does avoid heap reallocation. Move on std unique lock is permitted, while copy is not. So that varies. Commonly, resource owning types
  • Yakk - Adam Nevraumont
    Yakk - Adam Nevraumont over 5 years
    get performance from move as they can avoid duplicating the resource (be it on the free store or elsewhere); in other cases move permits a semantic action that copy does not (compare auto ptr to unique ptr, or unique lock, or shared lock; the modern types block copy for semantic reasons).