Search code examples
c++c++11movemove-semanticsstdmove

How does std::move invalidates the value of original variable?


In the following examples from cpp reference:

#include <iostream>
#include <utility>
#include <vector>
#include <string>

int main()
{
    std::string str = "Hello";
    std::vector<std::string> v;

    // uses the push_back(const T&) overload, which means 
    // we'll incur the cost of copying str
    v.push_back(str);
    std::cout << "After copy, str is \"" << str << "\"\n";

    // uses the rvalue reference push_back(T&&) overload, 
    // which means no strings will be copied; instead, the
    // Contents of str will be moved into the vector.  This is
    // less expensive, but also means str might now be empty.
    v.push_back(std::move(str));
    std::cout << "After move, str is \"" << str << "\"\n";

    std::cout << "The contents of the vector are \"" << v[0]
              << "\", \"" << v[1] << "\"\n";
}

Using std::move may cause the original value be lost. To me, it looks like

v.push_back(std::move(str))

causes a new member v[1] being created. Then,

&v[1] = &str

But why should it damage the value in str? It does not make sense.

There are many complicated tutorials about std::move which are harder than my own question to understand.

Could any one please write

v.push_back(std::move(str))

in its equivalent using c++03?

I look for an explanation whose understanding is easy and do not contain prerequisites such as x-value , static_cast, remove_reference, etc, as they themselves require to understand std::move first. Please avoid this circular dependency.

Also these links do not answer my question: 7510182, 3413470

Because I am interested in knowing how str is harm and not what happens to v[1].

Pseudo code is also welcome as far as it is as simple as c++03.


Update: To avoid complication, let's consider a simpler example of int as follows

int x = 10;
int y = std::move(x);
std::cout << x;

Solution

  • Depending on the implementation, the std::move could be a simple swap of the internal memory addresses.

    If you run the following code on http://cpp.sh/9f6ru

    #include <iostream>
    #include <string>
    
    int main()
    {
      std::string str1 = "test";
      std::string str2 = "test2";
    
      std::cout << "str1.data() before move: "<< static_cast<const void*>(str1.data()) << std::endl;
      std::cout << "str2.data() before move: "<< static_cast<const void*>(str2.data()) << std::endl;
    
      str2 = std::move(str1);
      std::cout << "=================================" << std::endl;
    
      std::cout << "str1.data() after move: " << static_cast<const void*>(str1.data()) << std::endl;
      std::cout << "str2.data() after move: " << static_cast<const void*>(str2.data()) << std::endl;
    }
    

    You will get the following output:

    str1.data() before move: 0x363d0d8
    str2.data() before move: 0x363d108
    =================================
    str1.data() after move: 0x363d108
    str2.data() after move: 0x363d0d8
    

    But the result may vary depending on the implementation of the compiler and the std library.

    But the implementation details can be even more complex http://cpp.sh/6dx7j. If you look at your example, then you will see that creating a copy for a string does not necessarily require that new memory for its content is allocated. This is because nearly all operations on std::string are read only or require the allocation of memory. So the implementation can decide to do just shallow copies:

    #include <iostream>
    #include <string>
    #include <vector>
    
    int main()
    {
      std::string str = "Hello";
      std::vector<std::string> v;
    
      std::cout << "str.data() before move: "<< static_cast<const void*>(str.data()) << std::endl;
    
      v.push_back(str);
      std::cout << "============================" << std::endl;
      std::cout << "str.data()  after push_back: "<< static_cast<const void*>(str.data()) << std::endl;
      std::cout << "v[0].data() after push_back: "<< static_cast<const void*>(v[0].data()) << std::endl;
    
      v.push_back(std::move(str));
      std::cout << "============================" << std::endl;
    
      std::cout << "str.data()  after move: "<< static_cast<const void*>(str.data()) << std::endl;
      std::cout << "v[0].data() after move: "<< static_cast<const void*>(v[0].data()) << std::endl;
      std::cout << "v[1].data() after move: "<< static_cast<const void*>(v[1].data()) << std::endl;
      std::cout << "After move, str is \"" << str << "\"\n";
    
    
      str = std::move(v[1]);
      std::cout << "============================" << std::endl;
      std::cout << "str.data()  after move: "<< static_cast<const void*>(str.data()) << std::endl;
      std::cout << "v[0].data() after move: "<< static_cast<const void*>(v[0].data()) << std::endl;
      std::cout << "v[1].data() after move: "<< static_cast<const void*>(v[1].data()) << std::endl;
      std::cout << "After move, str is \"" << str << "\"\n";
    }
    

    The output is

    str.data() before move: 0x3ec3048
    ============================
    str.data()  after push_back: 0x3ec3048
    v[0].data() after push_back: 0x3ec3048
    ============================
    str.data()  after move: 0x601df8
    v[0].data() after move: 0x3ec3048
    v[1].data() after move: 0x3ec3048
    After move, str is ""
    ============================
    str.data()  after move: 0x3ec3048
    v[0].data() after move: 0x3ec3048
    v[1].data() after move: 0x601df8
    After move, str is "Hello"
    

    And if you take a look at:

    #include <iostream>
    #include <string>
    #include <vector>
    
    int main()
    {
      std::string str = "Hello";
      std::vector<std::string> v;
    
      std::cout << "str.data() before move: "<< static_cast<const void*>(str.data()) << std::endl;
    
      v.push_back(str);
      std::cout << "============================" << std::endl;
      str[0] = 't';
      std::cout << "str.data()  after push_back: "<< static_cast<const void*>(str.data()) << std::endl;
      std::cout << "v[0].data() after push_back: "<< static_cast<const void*>(v[0].data()) << std::endl;
    
    }
    

    Then you would assume that str[0] = 't' would just replace the data in place. But this is not necessarily the case http://cpp.sh/47nsy.

    str.data() before move: 0x40b8258
    ============================
    str.data()  after push_back: 0x40b82a8
    v[0].data() after push_back: 0x40b8258
    

    And moving primitives like:

    void test(int i) {
      int x=i;
      int y=std::move(x);
      std::cout<<x;
      std::cout<<y;
    }
    

    Would be mostly be optimized out completely by the compiler:

      mov ebx, edi
      mov edi, offset std::cout
      mov esi, ebx
      call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
      mov edi, offset std::cout
      mov esi, ebx
      pop rbx
      jmp std::basic_ostream<char, std::char_traits<char> >::operator<<(int) # TAILCALL
    

    Both std::cout used the same register, the x and y are completely optimized away.