0

Background

I read the following answers earlier today, and it felt like relearning C++, litterally.

What are move semantics?

What is the copy-and-swap idiom?

Then I wondered if I should change my "ways" to use these exciting features; the main concerns I have are for code efficiency and clarity (former slightly more important than the latter to me). This lead me to this post:

Why have move semantics?

with which I strongly disagree (I agree with the answer, that is); I don't think a smart use of pointers could ever make move semantics redundant, neither in terms of efficiency nor clarity.

Question

Currently, whenever I implement a non-trivial object, I roughly do this:

struct Y
{
    // Implement
    Y();
    void clear();
    Y& operator= ( const& Y );

    // Dependent
    ~Y() { clear(); }

    Y( const Y& that )
        : Y()
    {
        operator=(that);
    }

    // Y(Y&&): no need, use Y(const Y&)
    // Y& operator=(Y&&): no need, use Y& operator=(const Y&)
};

From what I understand from the two first posts I read today, I am wondering whether it would be beneficial to change to this instead:

struct X
{
    // Implement
    X();
    X( const X& );

    void clear();
    void swap( X& );

    // Dependent
    ~X() { clear(); }

    X( X&& that )
        : X()
    {
        swap(that);
        // now: that <=> X()
        // and that.~X() will be called shortly
    }

    X& operator= ( X that ) // uses either X( X&& ) or X( const X& )
    { 
        swap(that); 
        return *this; 
        // now: that.~X() is called
    }

    // X& operator=(X&&): no need, use X& operator=(X)
};

Now, aside from being slightly more complicated and verbose, I don't see a situation in which the second (struct X) would yield a performance improvement, and I find that it is also less readable. Assuming my second code is using move-semantics correctly, how would it improve my current "way" of doing things (struct Y)?


Note 1: The only situation which I think makes the latter clearer is for "moving out of function"

X foo()
{
    X automatic_var;
    // do things
    return automatic_var;
}
// ...
X obj( foo() );

for which I think the alternative using std::shared_ptr, and std::reference_wrapper if I get tired of get()

std::shared_ptr<Y> foo()
{
    std::shared_ptr<Y> sptr( new Y() );
    // do things
    return sptr;
}
// ...
auto sptr = foo();
std::reference_wrapper<Y> ref( *ptr.get() );

is only slightly less clear, but as efficient.

Note 2: I really made an effort to make this question precise and answerable, and not subject to discussion; please think it through and don't interpret it as "Why are move-semantics useful", this is not what I am asking.

Community
  • 1
  • 1
Jonathan H
  • 6,750
  • 5
  • 39
  • 68
  • 1
    Explain downvote please? – Jonathan H Feb 04 '15 at 02:13
  • 2
    I literally just fixed this in my code. We were sorting 50000 objects that contained a map of properties, and it took ~15 seconds. I added a move assignment, and it's suddenly ~0.1 seconds. It changed an estimated 9750000 heap allocations to zero. – Mooing Duck Feb 05 '15 at 00:16
  • @MooingDuck Thank you for sharing this :) Practical evidence is always a good motivation. – Jonathan H Feb 05 '15 at 09:53

4 Answers4

1

Currently, whenever I implement a non-trivial object, I roughly do this...

I trust you abandon that when there are more complex data members - e.g. types that perform some calibration, data generation, file I/O, or resource acquisition during default construction, only to be thrown away / released on (re)assignment.

I don't see a situation in which the second (struct X) would yield a performance improvement.

Then you don't understand move semantics yet. I assure you such a situation exists. But given '"Why are move-semantics useful", this is not what I am asking.' I'm not going to explain them to you again here in the context of your own code... go "please think it through" yourself. If thinking fails you again, try adding a std::vector<> to many MBs of data and benchmark.

Tony Delroy
  • 94,554
  • 11
  • 158
  • 229
  • Fair enough, I deserve the rant. If you don't mind a question though: "try adding a `std::vector` to many MBs of data and benchmark", what is the meaning of "adding" in this sentence? – Jonathan H Feb 04 '15 at 14:20
  • 2
    @Sh3john: if you add say `std::vector data_;` as a data member in `class X`, and construct it with a large `count` - e.g. 1,000,000, then the difference between moving and copying instances of `X` will be very obvious in benchmarks. That would arm you with hands-on evidence that there really is *"...a situation in which the...`struct X`...would yield a performance improvement"*. Cheers. – Tony Delroy Feb 05 '15 at 03:10
1

std::shared_ptr stores your data on the free store (runtime overhead), and has a thread safe atomic increment/decrement (runtime overhead), and is nullable (either ignore it and get bugs, or check it constantly for runtime and programmer time overhead), and has a non-trivial to predict lifetime of the object (programmer overhead).

It is not in any way, shape or form as cheap as a move.

Move occurs when NRVO and other forms of elision fail, so if you have a cheap move using objects as values means you can rely on elision. Without cheap move, relying on elision is dangerous: elision is both fragile in practice and not guaranteed by the standard.

Having efficient move also makes containers of objects efficient without having to store containers of smart pointers.

A unique pointer solves some of the problems with shared pointer, except forced free store and nullability, and it also blocks easy use of copy construction.

As an aside, there are issues with your planned move-capable pattern.

First, you needlessly default construct before move constructing. Sometimes the default construct is not free.

Second operator=(X) does not play nice with some defects in the standard. I forget why -- composition or inheritance issue? -- I will try to remember to come back and edit it in.

If default construct is nearly free, and swapping is element-wise, here is a C++14 approach:

struct X{
  auto as_tie(){
    return std::tie( /* data fields of X here with commas */ );
  }
  friend void swap(X& lhs, X& rhs){
    std::swap(lhs.as_tie(), rhs.as_tie());
  }
  X();// implement
  X(X const&o):X(){
    as_tie()=o.as_tie();
  }
  X(X&&o):X(){
    as_tie()=std::move(o.as_tie());
  }
  X&operator=(X&&o)&{// note: lvalue this only
    X tmp{std::move(o)};
    swap(*this,o);
    return *this;
  }
  X&operator=(X const&o)&{// note: lvalue this only
    X tmp{o};
    swap(*this,o);
    return *this;
  }
};

now if you have components that need manual copying (like a unique_ptr) the above does not work. I'd just write a value_ptr myself (that is told how to copy) to keep those details away data consumers.

The as_tie function also makes == and < (and related) easy to write.

If X() is non-trivial, both X(X&&) and X(X const&) can be written manually and efficiency regained. And as operator=(X&&) is so short, having two of them is not bad.

As an alternative:

X& operator=(X&&o)&{
  as_tie()=std::move(o.as_tie());
  return *this;
}

is another implementation of = that has its pluses (and ditto for const&). It can be more efficient in some cases, but has worse exception safety. It also eliminates the need for swap, but I would leave swap in regardless: element-wise swap is worth it.

b4hand
  • 8,601
  • 3
  • 42
  • 48
Yakk - Adam Nevraumont
  • 235,777
  • 25
  • 285
  • 465
  • *"Second operator=(X) does not play nice with some defects in the standard. I forget why -- composition or inheritance issue? -- I will try to remember to come back and edit it in."* - were you thinking of the slicing issue, then failure to use a derived type's virtual functions? Not sure I'd call that a defect in the Standard though.... – Tony Delroy Feb 04 '15 at 05:08
  • People are moving back away from copy-and-swap for assignment operators, because it has a tendency to force heap allocations (vector/string/etc), whereas a straight copy doesn't, and thus copy-and-swap is far slower than a regular copy assignment. http://scottmeyers.blogspot.com/2014/06/the-drawbacks-of-implementing-move.html – Mooing Duck Feb 05 '15 at 00:04
  • 1
    @MooingDuck reverted your edit, added `this` to comments. Is comment clearer now? – Yakk - Adam Nevraumont Feb 05 '15 at 01:45
  • 1
    @Yakk I've been thinking about _"you needlessly default construct before move constructing"_ but (again for non-trivial objects) I think it is essential; the move constructor should leave the rvalue in _some_ valid state, and this is easily done by calling the default constructor before swap. Of course I'm assuming defcon is cheap here. If it is not cheap, we have a similar problem to what motivated copy-and-swap; either copy- or clear- "logic" will have to be included in the move constructor to disable the input while leaving it in a valid, destructible-state. Am I wrong? – Jonathan H Feb 05 '15 at 10:05
  • 2
    @Sh3ljohn so, if you don't via swap, you can sometimes just steal state from the right. This can sometimes be done without the default construct. The right hand side could be left in a valid-to-destroy state that is easier to reach than the default constructed state. I suppose that is sort of a corner case. – Yakk - Adam Nevraumont Feb 05 '15 at 14:02
1

Adding to what others have said:

You should not implement a constructor by calling operator= . You do that with:

Y( const Y& that ) : Y()
{
    operator=(that);
}

The reason to avoid this is that it requires default-constructing a Y (which doesn't even work if Y doesn't have a default constructor); and also it will pointlessly create and destroy any resources which might be allocated by the default constructor.

You correctly fix this for X by using the copy-and-swap idiom but then you introduce a similar mistake:

X( X&& that ) : X()
{
    swap(that);

The move-constructor should construct, not swap. Again there is a pointless default-construction and then destruction of any resources that the default constructor might have allocated.

You will have to actually write the move constructor to move each member. It needs to be correct so that your unified copy/move-assignment works.


A more general comment: you should be doing all of this very rarely. This is how you create a RAII wrapper for something; your more complicated objects should be made out of RAII-compatible subobjects so that they can follow the Rule of Zero. In most cases there are pre-existing wrappers such as shared_ptr so you do not have to write your own.

M.M
  • 130,300
  • 18
  • 171
  • 314
0

One notable improvement is when a function 'consumes' or otherwise works with a parameter (ie: takes by value rather than by reference). In that case, without the move semantics, passing in some lvalue as a parameter would force a copy.

The move semantics give the caller the choice of having the function copy the value (despite being potentially costly) or 'move' the value into the function knowing that after this call it has no more use for the lvalue which was passed in.

Additionally if a function uses a shared_ptr (or other wrapper which requires an allocation) just for the sake of being able to move the return value out, using move semantics then gets rid of that allocation. So it's more than just a nicety but also a perf boost.

While it is quite easy to write code that does not make use of move semantics and retains efficiency, using move semantics allows the users of the type which supports them to have simpler interfaces and use cases (by basically always using value semantics).

EDIT:

Since the OP specifically calls out efficiency, it's worth adding that move semantics can at best reach the same efficiency as could be achieved without them.

As far as I know, for any given code snippet where the addition of std::move would give a performance benefit vs not having it, simply reworking the code can match the best efficiency of the move or even beat it.

One recent bit of code I wrote was a producer which iterated over some data, selecting and transforming to fill up a buffer for a consumer. I wrote a little generic gimmick of sorts which abstracted capturing the data and shunting it off to a consumer.

The consumer was either same-thread (ie: dealt with as the producer piped it through), another thread (standard single-thread consumer deal), or multi-thread for PC platform.

In this case the producer code looked rather neat and simple as it filled a container and std::move'd it into the consumer via the aforementioned mechanism which was defined per platform.

If one were to suggest that this could be done to the same effect without using move semantics, one would be quite right. It just afforded me to have - what I thought - was simpler code. Ordinarily the price I'd pay is yet more cruft in the definition of the object, but in this case it was a std container so someone else did that work ;)

qeadz
  • 1,319
  • 8
  • 16
  • @Sh3ljohn "arguing" about "simpler" or "better" or "more/less readable" is not really in the spirit of SO. – Drew Dormann Feb 04 '15 at 02:10
  • ¶1: it seems to me that the solution is obvious -- that is, pass by reference when needed. ¶2: const lvalue references can bind to rvalues. ¶3: point taken, I discuss this in my first note. ¶4: would you be able to provide an example of such use-case (other than 3)? – Jonathan H Feb 04 '15 at 02:15
  • @DrewDormann I removed the last part of my comment about "arguing" in paragraph 4, and I think your comment also applies to this paragraph. My question is precise though; in what situation other than "move out of function" will `X` perform faster than `Y` and/or yield clearer code? AFA more/less readable is concerned, I disagree, I see plenty of posts mention code maintenance concerns, style and good practices. – Jonathan H Feb 04 '15 at 02:21
  • @Sh3ljohn I see that. I think what you call "moving out of function" is commonly called *a temporary* or *an rvalue*. For any non-trivial type, every temporary value is moved out of a function. In which case, you've answered your own question. Move semantics are useful for temporaries. – Drew Dormann Feb 04 '15 at 02:29