I was investigating the performance of moving std::string
. For the longest time, I've regarded string moves as almost free, thinking the compiler will inline everything and it will only involve a few cheap assignments.
In fact, my mental model for moving is literally
string& operator=(string&& rhs) noexcept
{
swap(*this, rhs);
return *this;
}
friend void swap(string& x, string& y) noexcept
{
// for disposition only
unsigned char buf[sizeof(string)];
memcpy(buf, &x, sizeof(string));
memcpy(&x, &y, sizeof(string));
memcpy(&y, buf, sizeof(string));
}
To the best of my understanding, this is a legal implementation if the memcpy
is changed to assigning individual fields.
It is to my great surprise to find gcc's implementation of moving involves creating a new string and might possibly throw due to the allocations despite being noexcept
.
Is this even conforming? Equally important, should I not think moving is almost free?
Bewilderingly, std::vector<char>
compiles down to what I'd expect.
clang's implementation is much different, although there is a suspicious std::string::reserve
Yes, std::string (since C++11) is able to be moved i.e. it supports move semantics.
std::move is used to indicate that an object t may be "moved from", i.e. allowing the efficient transfer of resources from t to another object. In particular, std::move produces an xvalue expression that identifies its argument t . It is exactly equivalent to a static_cast to an rvalue reference type.
Strings in C++ can be defined in two ways: using the string class or character arrays. It is more convenient to use string objects over character arrays. The string class provides many functions for the string objects. We can take string inputs from the user using the cin keyword or getline() function.
std::move is actually just a request to move and if the type of the object has not a move constructor/assign-operator defined or generated the move operation will fall back to a copy.
I've only analyzed GCC's version. Here's what's going on: the code handles different kind of allocators. If the allocator has the trait of _S_propagate_on_move_assign
or _S_always_equal
, then the move is almost free, as you expect. This is the if
in move operator=
:
if (!__str._M_is_local()
&& (_Alloc_traits::_S_propagate_on_move_assign()
|| _Alloc_traits::_S_always_equal()))
// cheap move
else assign(__str);
If the condition is true (_M_is_local()
means small string, description here), then the move is cheap.
If it is false, then it calls normal assign
(not the moving one). This is the case when either:
assign
will do a simple memcpy (cheap)What does this mean?
It means, that if you use the default allocator (or any allocator with traits mentioned earlier), then the move is still almost free.
On the other hand, the generated code is unnecessarily huge, and can be improved I think. It should have a separate code for handling usual allocators, or have a better assign
code (the problem is that assign
doesn't check for _M_is_local()
, but it does a capacity check, so the compiler cannot decide whether an allocation is needed or not, so it puts the allocation codepath into the executable unnecessarily - you can check out the exact details in the source code).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With