Is `std::exchange(obj, {})` common enough to be spelled `std::move_and_reset(obj)`?

95 views
Skip to first unread message

Marc Mutz

unread,
Dec 15, 2017, 5:14:08 PM12/15/17
to ISO C++ Standard - Future Proposals
Hi,

many examples using std::exchange pass a default-constructed object as
the second argument:

for (auto &item : std::exchange(m_items, {}))

delete std::exchange(m_pImpl, {});

Object(Object &&other) : m_var(std::exchange(other.m_var, {})) {}

The exchange-with-{} operation would also supplement std::move, which
doesn't actually move. The new function always reliably moves (because
it returns by value).

The question is what name to give to that operation? Some ideas (in
alphabetical order):

- extract (like in node-based containers)
- move_and_reset
- move_and_empty
- reset_move
- take

What do people think?

Thanks,
Marc

Nicol Bolas

unread,
Dec 15, 2017, 6:19:47 PM12/15/17
to ISO C++ Standard - Future Proposals
On Friday, December 15, 2017 at 5:14:08 PM UTC-5, Marc Mutz wrote:
Hi,

many examples using std::exchange pass a default-constructed object as
the second argument:

    for (auto &item : std::exchange(m_items, {}))

    delete std::exchange(m_pImpl, {});

    Object(Object &&other) : m_var(std::exchange(other.m_var, {})) {}
The exchange-with-{} operation would also supplement std::move, which
doesn't actually move. The new function always reliably moves (because
it returns by value).

The question is what name to give to that operation?

No, the first question is whether we want people to do this.

I can understand why someone might use this idiom when deleting pointers. But the rest open up too many possibilities for inefficiencies.

Consider this (in tandem with P0408, which adds move support to string streams):

istringstream stream(std::move(some_string));

The `std::move` is just a cast; the actual move happens within `stream`'s constructor. By contrast, if you did you "move to prvalue" thing, that would perform two moves. It first must move to the prvalue, which manifests a temporary used as the constructor parameter. Then the constructor moves from that into the stream.

I don't mind people using this kind of `exchange` idiom. But if we have a function specifically for this, then we are effectively encouraging people to do it. Even when it's not a good idea.

Also, remember that `exchange` does two things. It moves from the original, but it also move into the original. In the first case, you don't care that `m_items` gets default initialized; you just want it to be moved-from. In the other two cases, default initialization is not merely a default; it's a fundamental part of the algorithm.

So you would need two functions: one that will move a default-constructed prvalue into the old object (and thus impose both DefaultConstructible and MoveAssignable requirements on it) and one which does not.

Marc Mutz

unread,
Dec 16, 2017, 7:51:18 PM12/16/17
to std-pr...@isocpp.org
On 2017-12-16 00:19, Nicol Bolas wrote:
[...]
> I don't mind people using this kind of `exchange` idiom. But if we
> have a function specifically for this, then we are effectively
> encouraging people to do it. Even when it's not a good idea.
>
> Also, remember that `exchange` does two things. It moves from the
> original, but it also move_ into_ the original. In the first case, you
> don't care that `m_items` gets default initialized; you just want it
> to be moved-from.

The first example was actually meant as a shortcut for

auto items = std::move(m_items);
m_items.clear(); // or = {}
for (auto &item : items)

which is a pattern common enough to be annoying without std::exchange
(or with CoW containers which detach in ranged for-loops *cough* Qt
*cough*).

for (auto &item : std::take(m_items)) // using the shortest function
name as a placeholder

> In the other two cases, default initialization is
> not merely a default; it's a fundamental part of the algorithm.
>
> So you would need two functions: one that will move a
> default-constructed prvalue into the old object (and thus impose both
> DefaultConstructible and MoveAssignable requirements on it) and one
> which does not.

I considered std::take (which does) and std::move (which does not) to be
those two functions.

I said "supplement std::move", not "replace std::move". The problem is
that the std uses the hand-waving "valid, but unspecified state" to
define the moved-from state, and that forces people to call clear() or
assign {} to a moved-from object to get it back into a defined state.

std::take() would leave the moved-from object in a defined state (the -
help me - value-initialized(?) one), while std::move() leaves the
moved-from object in an unspecified state, which is ok if you don't
indent to use the moved-from object later, except to assign a new value
or destroy it, but gets in the way otherwise.

If std::take() was function separate from std::exchange(), stdlib
implementations could overload it to avoid the assignment if they know
their implementation reaches the default-constructed state for
moved-from objects:

template <typename...Args>
auto take(std::vector<Args...> &v) {
return std::move(v); // post: v.empty() (I believe this holds
for libstd++'s vector, at least)
}

template <typename...Args>
auto take(std::basic_string<Args...> &s) {
auto r = std::move(s);
s.clear(); // deal with SSO
return r;
}

Thanks,
Marc



Nicol Bolas

unread,
Dec 16, 2017, 10:11:22 PM12/16/17
to ISO C++ Standard - Future Proposals


On Saturday, December 16, 2017 at 7:51:18 PM UTC-5, Marc Mutz wrote:
On 2017-12-16 00:19, Nicol Bolas wrote:
[...]
> I don't mind people using this kind of `exchange` idiom. But if we
> have a function specifically for this, then we are effectively
> encouraging people to do it. Even when it's not a good idea.
>
> Also, remember that `exchange` does two things. It moves from the
> original, but it also move_ into_ the original. In the first case, you
> don't care that `m_items` gets default initialized; you just want it
> to be moved-from.

The first example was actually meant as a shortcut for

    auto items = std::move(m_items);
    m_items.clear(); // or = {}
    for (auto &item : items)

which is a pattern common enough to be annoying without std::exchange

This exemplifies my point about the need for two functions. Do you really need `m_items` to be cleared, or do you just need to destroy its contents? There are times when you genuinely need the clearing, and there are times when you genuinely don't care.

(or with CoW containers which detach in ranged for-loops *cough* Qt
*cough*).

    for (auto &item : std::take(m_items)) // using the shortest function
name as a placeholder

> In the other two cases, default initialization is
> not merely a default; it's a fundamental part of the algorithm.
>
> So you would need two functions: one that will move a
> default-constructed prvalue into the old object (and thus impose both
> DefaultConstructible and MoveAssignable requirements on it) and one
> which does not.

I considered std::take (which does) and std::move (which does not) to be
those two functions.

But, as you pointed out, `std::move` does not move anything; it's just a cast. So `std::move` does not satisfy what I said.

The two functions I'm talking about both provoke an actual, honest-to-God move. There's:

template<typename T>
auto move_and_clear(T &t)
{
  T new_t
= std::move(t);
  t
= T{};
 
return new_t;
}

And there's:

template<typename T>
auto force_move(T &t)
{
 
return T(std::move(t));
}

They both provoke a move. But one leaves the object in the "moved-from" state, while the other leaves it in a very well defined state (assigned from a default-constructed object).

I said "supplement std::move", not "replace std::move". The problem is
that the std uses the hand-waving "valid, but unspecified state" to
define the moved-from state, and that forces people to call clear() or
assign {} to a moved-from object to get it back into a defined state.

And what's wrong with that? Most of the time when you're moving something, you don't care what state the moved-from object is in. Since more often than not, you're done with it.

The purpose of `force_move` is to ensure that a move actually happens. Some functions that you might pass an rvalue reference to may not actually move from it. But you may no longer want to keep the object's contents around; you want the receiver to take it. So by using `force_move`, you ensure that either the receiver moved from it or the temporary that was manifested will destroy the data in the object at the end of that expression.

Either way, by the time the next statement starts, you're guaranteed that the data is gone.
 

Marc Mutz

unread,
Dec 17, 2017, 8:21:31 AM12/17/17
to std-pr...@isocpp.org
Ok, I see what you mean. I'm not convinced about the need for
force_move(): Either you care about the state of the moved-from object
(that includes the case you included, where you want to ensure that any
resources it may have held are freed), then you use the new function, or
you don't, then you use std::move(). Yes, it might not actually move,
but you're not supposed to care, because not moving is a valid outcome
under the "valid, but unspecified state" rule, and something that
actually happens in practice, when the move decays to a copy (int, SSOed
string, ...).

Thanks,
Marc

Richard Hodges

unread,
Dec 17, 2017, 9:54:02 AM12/17/17
to std-pr...@isocpp.org
> The two functions I'm talking about both provoke an actual,
> honest-to-God move. There's:

> template<typename T>
> auto move_and_clear(T &t)


I've been thinking along these lines myself. I'm glad you brought it up.

However the implementation for containers should preserve any previously-allocated implementation memory so as not to cause unneccessary news and deletes.

Here's one possible implementation, in terms of a std::clear() which delegates to a specialised template functor to "do the right thing" for any type.

#include <vector>
#include <cassert>


namespace std {

    // general case for anything default-constructable and moveable
    template<class T> struct specific_clear_operation
    {
        template<class Arg> void operator()(Arg&& arg) const
        {
            arg = T();
        }
    };

    // specialise for containers.. (or perhaps anything with a .clear() method?)
    template<class T, class A> struct specific_clear_operation<std::vector<T, A>>
    {
        template<class Arg> void operator()(Arg&& arg) const
        {
            arg.clear();
        }
    };

    template<class T> T& clear(T& arg)
    {
        auto op = specific_clear_operation<std::decay_t<T>>();
        op(arg);
        return arg;
    }

    template<class T> T move_and_clear(T& src)
    {
        auto ret = std::move(src);
        clear(src);
        return ret;
    }
}


int main()
{
    double x = 10.0;
    double y = std::move_and_clear(x);
    assert(x == 0.0);
    assert(y == 10.0);

    auto v = std::vector { 1,2,3,4,5 };
    auto w = std::move_and_clear(v);
    assert(std::size(v) == 0);
    assert(std::size(w) == 5);
}




--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+unsubscribe@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/e95e3edc705db04d07f66674448488bc%40kdab.com.

Nicol Bolas

unread,
Dec 17, 2017, 10:23:14 AM12/17/17
to ISO C++ Standard - Future Proposals
It's not that you care about the state of the moved-from object. It's that you care about the state of the data that the possibly-moved-from object had. You want to know that the data it stored is either in use or has been destroyed. Leaving it in the old object is not the right choice.

Consider something like `optional::value_or`. It takes a forwarding reference to the "or" part, which it may or may not move from. You don't really care about the "or" object itself; what you care about is the stuff it contains. If it allocates memory, then you want that memory to either be in the return value or be deallocated. You don't want it to be in the old object.

Now I admit that `value_or` is not a good example because most uses where you're actually moving from it will likely be given a prvalue temporary (and thus be destroyed either way at the end of the statement). But there are other cases out there where you might move from some object or not, as the case may be.

Nicol Bolas

unread,
Dec 17, 2017, 10:26:28 AM12/17/17
to ISO C++ Standard - Future Proposals


On Sunday, December 17, 2017 at 9:54:02 AM UTC-5, Richard Hodges wrote:
> The two functions I'm talking about both provoke an actual,
> honest-to-God move. There's:

> template<typename T>
> auto move_and_clear(T &t)


I've been thinking along these lines myself. I'm glad you brought it up.

However the implementation for containers should preserve any previously-allocated implementation memory so as not to cause unneccessary news and deletes.

If you move from a container, any "previously-allocated implementation memory" should have been transferred to the new object, right? So what "news and deletes" would we be talking about?

Reply all
Reply to author
Forward
0 new messages