Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 389
Don Hatch Avatar asked Jun 24 '16 11:06

Don Hatch


2 Answers

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

like image 174
WhiZTiM Avatar answered Oct 26 '22 16:10

WhiZTiM


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);
like image 33
Angew is no longer proud of SO Avatar answered Oct 26 '22 16:10

Angew is no longer proud of SO