Quantcast
Channel: C++ Archives - Crascit
Viewing all articles
Browse latest Browse all 10

Move constructors can copy and std::move doesn’t move anything

$
0
0

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 containers a and b of a standard container type other than array, shall exchange the values of a and b 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 after std::swap is called.
  • The contents of m_items will be transferred to items 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.


Viewing all articles
Browse latest Browse all 10

Trending Articles