Don Hatch
Don Hatch

Reputation: 5402

is it possible to implement a std::move-and-clear function?

Is it possible to write a function move_and_clear such that, for any STL container:

do_something_with(move_and_clear(container));

is equivalent to:

do_something_with(std::move(container));
container.clear();

?

Here is my first attempt, which doesn't work. I think I got the types right (although a production version of this would probably sprinkle in some std::remove_reference's), and it compiles successfuly, but it fails or crashes because scratch is accessed after it goes out of scope.

template<class T>
T &&move_and_clear(T &t)
{
    T scratch;
    std::swap(scratch, t);
    return std::move(scratch);
}

Here is my second attempt. This actually works, but it's a preprocessor macro, and is therefore evil:

template<class T>
T &&move_and_clear_helper(T &t, T &&scratch)
{
    std::swap(scratch, t);
    return std::move(scratch);
}
#define move_and_clear(t) move_and_clear_helper(t, decltype(t)())

My third attempt is another macro which also works, this time using a lambda instead of a named helper function. So it's a bit more self-contained than the previous macro, but perhaps less readable, and of course it's still evil because it's a macro:

#define move_and_clear(t) \
    [](decltype(t) &tparam, decltype(t) &&scratch){ \
        std::swap(scratch, tparam); \
        return std::move(scratch); \
    }(t, decltype(t)())

Here is a compilable program incorporating my three attempts:

/*
    g++ --std=c++11 -W -Wall -g move_and_clear.cc -o move_and_clear1 -DWHICH=1
    g++ --std=c++11 -W -Wall -g move_and_clear.cc -o move_and_clear2 -DWHICH=2
    g++ --std=c++11 -W -Wall -g move_and_clear.cc -o move_and_clear3 -DWHICH=3
    ./move_and_clear1   # assert-fails
    ./move_and_clear2   # succeeds
    ./move_and_clear3   # succeeds
*/

#include <assert.h>
#include <iostream>
#include <memory>
#include <vector>

#if WHICH == 1
    template<class T>
    T &&move_and_clear(T &t)
    {
        T scratch;
        std::swap(scratch, t);
        return std::move(scratch);
    }
#elif WHICH == 2
    template<class T>
    T &&move_and_clear_helper(T &t, T &&scratch)
    {
        std::swap(scratch, t);
        return std::move(scratch);
    }
    #define move_and_clear(t) move_and_clear_helper(t, decltype(t)())
#elif WHICH == 3
    #define move_and_clear(t) \
        [](decltype(t) &tparam, decltype(t) &&scratch){ \
            std::swap(scratch, tparam); \
            return std::move(scratch); \
        }(t, decltype(t)())
#endif

// Example "do_something_with":
// takes an rvalue reference to a vector that must have size 3,
// steals its contents, and leaves it in a valid but unspecified state.
// (Implementation detail: leaves it with 7 elements.)
template<typename T>
void plunder3_and_leave_in_unspecified_state(std::vector<T> &&v)
{
  assert(v.size() == 3);
  std::vector<T> pirate(7);
  assert(pirate.size() == 7);
  std::swap(pirate, v);
  assert(pirate.size() == 3);
  assert(v.size() == 7);
}

int main(int, char**)
{
    {
        std::cout << "Using std::move and clear ..." << std::endl << std::flush;
        std::vector<std::unique_ptr<int>> v(3);
        assert(v.size() == 3);
        plunder3_and_leave_in_unspecified_state(std::move(v));
        assert(v.size() == 7); // (uses knowledge of plunder's impl detail)
        v.clear();
        assert(v.empty());
        std::cout << "done." << std::endl << std::flush;
    }
    {
        std::cout << "Using move_and_clear ..." << std::endl << std::flush;
        std::vector<std::unique_ptr<int>> v(3);
        assert(v.size() == 3);
        plunder3_and_leave_in_unspecified_state(move_and_clear(v));
        assert(v.empty());
        std::cout << "done." << std::endl << std::flush;
    }
}

Is there a way to implement move_and_clear as a template function, without using a macro?

Upvotes: 8

Views: 2423

Answers (4)

WhiZTiM
WhiZTiM

Reputation: 21576

Is it possible to write a function move_and_clear such that, for any STL container:

do_something_with(move_and_clear(container));

is equivalent to:

do_something_with(std::move(container));
container.clear();

?

template<typename T>
T move_and_clear(T& data){
     T rtn(std::move(data));
     data.clear();
     return rtn;
}

The return value will be treated as an rvalue at the call site.

Again, that will enjoy the benefits of Return Value Optimization (in any sane compiler). And most definitely, inlining. See Howard Hinnant's answer to this question.


Again, STL containers have move constructors, but for any other custom container, it's better to constrain it to move-constructible types. Else, you may call it with a conatiner that doesn't move, and you have an necessary copy there.

template<typename T>
auto move_and_clear(T& data)
-> std::enable_if_t<std::is_move_constructible<T>::value, T>
{
     T rtn(std::move(data));
     data.clear();
     return rtn;
}

See: This answer

EDIT:

If you have fears about RVO, I don't know any major compiler that wouldn't do an RVO there in optimized builds (except explicitly turned off by a switch). There is also a proposal to make it mandatory, hopefully we should see that in C++17.

EDIT2:

The paper made it into the Working Draft of C++17, See this

Upvotes: 12

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275340

template<class T>
T &&move_and_clear(T &t)
{
  T scratch;
  std::swap(scratch, t);
  return std::move(scratch);
}

this is close. You typed to much however:

template<class T>
T move_and_clear(T &t)
{
  T scratch;
  std::swap(scratch, t);
  return scratch;
}

scratch will be elided with the return value (barring someone setting hostile compiler flags).

A slight improvement:

template<class T>
T move_and_clear(T &t)
{
  T scratch;
  using std::swap;
  swap(scratch, t);
  return scratch;
}

the enables Koenig lookup on swap, which means if someone wrote a free swap operation on their container that is more efficient than std::swap, it would be called instead.


Now, in practice, a successful move of a std container will clear it. The only one where not clearing the source container makes even a lick of sense is std::basic_string when the SSO (small string optimization) is active, and there the effort required to do the clear is so tiny I cannot see a library writer not doing it for the sake of sanity.

However, this code with the swap guarantees that the source object will have the state of an empty object of type T.

Note that this does not work in all possible C++ implementations:

template<class T>
T move_and_clear(T &t)
{
  T scratch = std::move(t);
  return scratch;
}

For most std containers, the requirements on move-construction are more strict, and iterators must transfer. Iterators transfering means that it must clear the source in practice.

On std::basic_string, however, iterators do not have to transfer due to the small string optimization. I know of no guarantee that the source basic_string will be cleared (but I could easily be wrong: there could be a clause in the standard that explicitly requires that: it is just not implied by the other semantics of the operation).

Upvotes: 0

Here's an implementation which doesn't require an extra move of the container, but introduces a proxy object:

template <class T>
class ClearAfterMove
{
  T &&object;

public:
  ClearAfterMove(T &&object) : object(std::move(object)) {}

  ClearAfterMove(ClearAfterMove&&) = delete;

  ~ClearAfterMove() { object.clear(); }

  operator T&& () const { return std::move(object); }
};


template <class T>
ClearAfterMove<T> move_and_clear(T &t)
{
  return { std::move(t) };
}

How this works is that it creates a non-movable object ClearAfterMove, which will wrap the source container t, and call clear on t when it (the wrapper) goes out of scope.

In a call like this:

do_something_with(move_and_clear(container));

A temporary ClearAfterMove object (let's call it cam) wrapping container will be created by the call to move_and_clear. This temporary will then be converted to an rvalue reference by its conversion operator and passed on to do_something_with.

Temporaries go out of scope at the end of the full-expression in which they were created. For cam, this means it will be destroyed once the call to do_something_with is resolved, which is exactly what you want.

Note that this has an advantage of not producing any extra moves, and a disadvantage which all proxy object solutions have: it doesn't play nice with type deduction, such as:

auto x = move_and_clear(y);

Upvotes: 1

std::move is defined to move the content, so the source container would effectively be cleared.

(or perhaps I am mis-undestanding your question)

Upvotes: -2

Related Questions