I recently came across an interesting use of std::move
which looked something like the following:
void MyObject::processItems()
{
std::vector<int> items(std::move(m_items));
for (auto item : items)
{
// Do things which may add new items to m_items
}
}
The intent of the code was that every time the member function processItems()
was called, it would perform some operation on each item held in the member variable m_items
. Each processed item should be removed from m_items
. The operation to be performed might generate new items which would be added to m_items
, so care had to be taken about how to iterate over the set of items.
To ensure robust iteration over the items to be processed, the code transfers the contents of m_items
to a local object and then iterates over that local object. Thus, if new items are created during processing, they would be added to m_items
and the container being iterated over (items
) would not be affected. All good, right? Well, probably yes, but by no means is it guaranteed.
Not everything is as it seems
The problem with the above code is that it makes an assumption about the implementation of std::vector
‘s move constructor which is likely to be true, but is not guaranteed. Here is the problematic line:
std::vector<int> items(std::move(m_items));
The key assumption is that after this line, m_items
will be empty. That seems reasonable, the code even reads as though it says “move m_items
into items
“, but std::move
can be quite a misleading charlatan! Contrary to its name, on its own it doesn’t actually move anything. It is nothing more than a glorified static_cast
. In fact, that is exactly how the C++ standard defines it:
template <class T> typename remove_reference<T>::type&& move(T&& t) noexcept;
Returns:static_cast<typename remove_reference<T>::type&&>(t).
All std::move
is doing is casting its argument to an rvalue. The effect of this is to potentially change what the compiler selects as the recipient of std::move
‘s return value. In the above code, it makes the compiler select std::vector
‘s move constructor instead of its copy constructor. What may be suprising is that the while the C++ standard clearly defines what the move-constructed object must hold after construction, it is completely silent on the effect of the object from which it was supposedly moved. The wording of the post condition is as follows (where the wording refers to the syntax X u(rv)
and rv
is an rvalue):
u shall be equal to the value that rv had before this construction.
There’s no mention of what value rv
must hold after u
is constructed. Thus, for the example code at the beginning of this article, we can be sure that items
will indeed hold the array of items to be processed, but we cannot reliably assume that m_items
will be empty after std::vector
‘s move constructor has created items
. It is very likely that compiler implementations will indeed leave the moved-from container empty, but this is not guaranteed. A similar situation exists for other STL containers too, not just std::vector
.
std::swap as a clear alternative
One way we could restore defined behaviour to our example would be to explicitly clear m_items
after we construct items
, but this still leaves open the possibility that constructing items
could create a deep copy of m_items
if that’s how std::vector
‘s move constructor is implemented by the compiler (which would be entirely legal, albeit suboptimal). We’d much prefer to be certain that no copy is performed and indeed we can do so in a way that is 100% backed up by the C++ standard. The key is to make use of std::swap
instead of std::move
. Modifying our original example results in the following:
void MyObject::processItems()
{
std::vector<int> items;
std::swap(items, m_items);
for (auto item : items)
{
// Do things which may add new items to m_items
}
}
With this arrangement, we have very clear and unambiguous behaviour. We first create an empty container. We then swap it with m_items
and the C++ standard is much more helpful in defining what this must do. It says that std::swap
will forward to std::vector
‘s own swap
member function, which in turn is required to obey the following:
The expression
a.swap(b)
, for containersa
andb
of a standard container type other thanarray
, shall exchange the values ofa
andb
without invoking any move, copy, or swap operations on the individual container elements.
Since we swap an empty items
with m_items
, we thus have the following guarantees:
m_items
will be empty afterstd::swap
is called.- The contents of
m_items
will be transferred toitems
without any copying of the individual elements.
This is precisely what the original code intended to do but did not necessarily ensure. It is worth noting that the std::swap
approach extends well to arbitrary types too, not just STL containers. Consider a slightly modified version of our example, this time with MyObject
being a templated class where the container type is the template parameter:
template<typename T>
class MyObject
{
public:
// ... constructors, etc. omitted
void processItems()
{
T items;
std::swap(items, m_items);
for (auto item : items)
{
// ...
}
}
private:
T m_items;
};
In this case, we cannot assume anything about the container type, not even whether or not it has a move constructor. By using std::swap
, the code always provides clear behaviour, with typical cases making use of the container’s move constructor if it has one and falling back to the container’s copy constructor otherwise.
A final note on std::move
It may seem that std::move
was a poorly conceived (or at least poorly named) feature added with C++11. Much of that comes from misunderstanding what problems it was designed to solve. One way to think of std::move
may be to take it as saying “I don’t care what value this thing holds after here”. For an excellent explanation of std::move
and related topics, I highly recommend you check out Thomas Becker’s discussion on rvalue references. Scott Meyers’ recent publication Effective Modern C++ is also well worth a read, covering many interesting aspects of these matters and related material. Lastly, this StackOverflow answer gives a great summary of how we ended up with std::move
to begin with.
The post Move constructors can copy and std::move doesn’t move anything appeared first on Crascit.