C++ Logo


Advanced search

Re: std::take(obj), aka std::exchange(obj, {})

From: Giuseppe D'Angelo <giuseppe.dangelo_at_[hidden]>
Date: Sun, 27 Sep 2020 22:16:11 +0200

Il 25/09/20 05:01, Arthur O'Dwyer ha scritto:
> On Thu, Sep 24, 2020 at 7:56 PM Giuseppe D'Angelo
> <giuseppe.dangelo_at_[hidden] <mailto:giuseppe.dangelo_at_[hidden]>> wrote:
> Il 25/09/20 00:15, Arthur O'Dwyer ha scritto:
> > The big costs of `std::exchange(obj, {})` as far as I'm concerned
> are
> > that you have to default-construct a whole T object, pass
> std::exchange
> > a /reference/ to that object, call the assignment operator which
> has to
> > conditionally free the old resource, and finally destroy the
> temporary T
> > object (which has to conditionally free the resource). I can see
> how it
> > would be beneficial to coalesce some of these operations into a
> tighter
> > package like std::take. But I don't see how your suggested
> > implementation of std::take actually helps. You're still
> constructing a
> > temporary T, calling the assignment operator, and calling the
> destructor
> > of the temporary.
> >
> > // std::take of a string
> > {
> > std::string temp{};
> > newobj = std::move(obj);
> > obj = std::move(temp);
> > } // temp.~string();
> >
> > // what you want instead
> > newobj = std::move(obj);
> > obj.clear();
> I don't think these two are equivalent.
> They are for std::string, std::vector<T>, all other containers... You're
> right that the latter is not a generic solution (e.g. that code won't
> compile for smart pointers like std::unique_ptr<T>).

But below you've just proved that they're not equivalent for some of
these on some implementations; and they're not equivalent in general.

> That's exactly why I claim this is a problem for your current proposal.
> You propose that std::take should have the same semantic effect as
> "move-out and then clear," but the physical implementation strategy you

A bit stronger, "move out and reset to default constructed state", but
that's the gist.

> propose for it intentionally produces suboptimal codegen for many
> types. You propose a new library function to do a thing /*less
> efficiently*/ than the programmer could do it by hand. I think you
> should look for an implementation strategy that lets you produce the
> best possible codegen.
> However, I think this is all of questionable relevance, because capacity
> is not a /*salient*/ feature of std::string.

This criticism is more than fair, and I thank you for it. I think the
point(s) raised here boil down to a few key observations:

1) Is the proposed implementation the "best" available for a generic
type T of which we know nothing about?

A: I'd say a humble "yes" here.

2) Does such an implementation produce the optimal code for all T?
Does it only carry "salient state" (to quote the above)? Can an author
provide a better implementation for their classes?

A: The evidence suggests that an author can do better.

Thinking about it, take() is very similar to swap() in this regard. The
default swap() does a series of steps (1 move construction, 2 move
assignments, 1 destruction of a moved from object) that we recognize can
be suboptimal for a number of types and/or the compiler cannot easily
(or possibly) optimize it to the same degree where the class author can.

Using take() to construct a new object, as in

   T new_obj = take(old_obj);

does by default

   * 1 move construction
   * 1 default construction
   * 1 move assignment
   * 1 destruction of a moved from object
   * (+ 1 move/destruction, but NRVO applies, so that won't happen)

and some or all of these steps could be optimized out by the class'
author, depending on the needs (thinking of std::array, or pimpled
types, etc.). So, this points in the direction of making take() a
customization point (object).

Assignment from take() is a bit more tricky, but points in the same
direction. The fundamental difference between

   list2 = take(list1);


   list2 = std::move(list1);

is that this op= acts on both list1 and list2, while take doesn't know
about list2. So I need a new operation here -- take_assign(target,
source) or somesuch -- to make it be as efficient as a move-assignment /
swap+clear / etc. And this should be a CPO as well. With this available,
we get the desired performance back "out of the box"...

> https://godbolt.org/z/37G8hT

I'll go and think a bit more.

Giuseppe D'Angelo | giuseppe.dangelo_at_[hidden] | Senior Software Engineer
KDAB (France) S.A.S., a KDAB Group company
Tel. France +33 (0)4 90 84 08 53, http://www.kdab.com
KDAB - The Qt, C++ and OpenGL Experts

Received on 2020-09-27 15:16:27