Search code examples
c++c++11vectormovervalue-reference

Confused by how the rvalue reference vector::push_back function increases effciency


From the cppreference.com, I found a simple example about using the std::move:

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);                                    // First push_back
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));                        // Second push_back

The comment says that string copy was avoided.

The first push_back will call: void push_back(const value_type& _Val)

The second push_back will call: void push_back(value_type&& _Val)

I checked the implementation code of the two functions:

void push_back(const value_type& _Val)
    {   // insert element at end
    if (_Inside(_STD addressof(_Val)))
        {   // push back an element
        size_type _Idx = _STD addressof(_Val) - _Unfancy(this->_Myfirst());
        if (this->_Mylast() == this->_Myend())
            _Reserve(1);
        _Orphan_range(this->_Mylast(), this->_Mylast());
        this->_Getal().construct(_Unfancy(this->_Mylast()),
            this->_Myfirst()[_Idx]);
        ++this->_Mylast();
        }
    else
        {   // push back a non-element
        if (this->_Mylast() == this->_Myend())
            _Reserve(1);
        _Orphan_range(this->_Mylast(), this->_Mylast());
        this->_Getal().construct(_Unfancy(this->_Mylast()),
            _Val);
        ++this->_Mylast();
        }
    }

and

void push_back(value_type&& _Val)
    {   // insert by moving into element at end
    if (_Inside(_STD addressof(_Val)))
        {   // push back an element
        size_type _Idx = _STD addressof(_Val) - _Unfancy(this->_Myfirst());
        if (this->_Mylast() == this->_Myend())
            _Reserve(1);
        _Orphan_range(this->_Mylast(), this->_Mylast());
        this->_Getal().construct(_Unfancy(this->_Mylast()),
            _STD forward<value_type>(this->_Myfirst()[_Idx]));
        ++this->_Mylast();
        }
    else
        {   // push back a non-element
        if (this->_Mylast() == this->_Myend())
            _Reserve(1);
        _Orphan_range(this->_Mylast(), this->_Mylast());
        this->_Getal().construct(_Unfancy(this->_Mylast()),
            _STD forward<value_type>(_Val));
        ++this->_Mylast();
        }
    }

So, according to my understanding, both first push_back (v.push_back(str);) and second push_back (v.push_back(std::move(str));) will trigger the vector to construct a std::string type variable and attach it to the vector.

So, actually in both push_back calls, string was not copied. And, for both push_back, the expense are the same, because both calls basically do the same thing except the second push_back will make the str input to be empty.

As for the efficiency, the only difference I can think of is that the second push_back will not call the delete []cstring; in the destructor of std::string which makes the second push_back call a little bit more efficient.

Not sure if my understanding is correct. Thanks a lot!


Solution

  • The difference is here:

        this->_Getal().construct(_Unfancy(this->_Mylast()),
            _STD forward<value_type>(_Val));
    

    vs

        this->_Getal().construct(_Unfancy(this->_Mylast()),
            _Val);
    

    Now that forward<value_type> in effect calls std::move on the std::string.

    In one case, we construct a std::string by std::string const&, in the other by a std::string &&.

    So to see the difference, we have to examine what those two different constructors do.

    std::string is usually implemented with the SBO (small buffer optimization). If the string is short (a dozen or so characters), the string is stored within the std::string.

    If it is longer, instead the std::string stores a pointer to it.

    If you have SBO active, both move and copy copy the bytes over. Move may then clear the source.

    std::string(std::string const&) in the case of non-SBO does an allocation and duplicates the buffer containing the characters.

    std::string(std::string &&) in the case of non-SBO moves the pointer over to the destination object, and empties the source object. No memory allocation occurs, and zero bytes are copied.

    This is what the push_back(&&) overload provides.