16

Consider:

#include <cstdlib>
#include <memory>
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>
using namespace std;

class Gizmo
{
public:
    Gizmo() : foo_(shared_ptr<string>(new string("bar"))) {};
    Gizmo(Gizmo&& rhs); // Implemented Below

private:
    shared_ptr<string> foo_;
};

/*
// doesn't use std::move
Gizmo::Gizmo(Gizmo&& rhs)
:   foo_(rhs.foo_)
{
}
*/


// Does use std::move
Gizmo::Gizmo(Gizmo&& rhs)
:   foo_(std::move(rhs.foo_))
{
}

int main()
{
    typedef vector<Gizmo> Gizmos;
    Gizmos gizmos;
    generate_n(back_inserter(gizmos), 10000, []() -> Gizmo
    {
        Gizmo ret;
        return ret;
    });

    random_shuffle(gizmos.begin(), gizmos.end());

}

In the above code, there are two versions of Gizmo::Gizmo(Gizmo&&) -- one uses std::move to actually move the shared_ptr, and the other just copies the shared_ptr.

Both version seem to work on the surface. One difference (the only difference I can see) is in the non-move version the reference count of the shared_ptr is temporarily increased, but only briefly.

I would normally go ahead and move the shared_ptr, but only to be clear and consistent in my code. Am I missing a consideration here? Should I prefer one version over the other for any technical reason?

Fraser
  • 74,704
  • 20
  • 238
  • 215
John Dibling
  • 99,718
  • 31
  • 186
  • 324
  • 2
    Moving in a move constructor is at least semantically consistent... – ildjarn Jun 08 '12 at 17:24
  • 1
    Why do you keep the string in a shared_ptr? A shared_ptr as a member-variable most often are a sign of bad design. – Viktor Sehr Jun 08 '12 at 17:25
  • 1
    Moving in a move constructor is in line with what the compiler would automatically generate. – R. Martinho Fernandes Jun 08 '12 at 17:27
  • @Viktor this is just an example. – John Dibling Jun 08 '12 at 17:37
  • 11
    @ViktorSehr: "A shared_ptr as a member-variable most often are a sign of bad design." Why do you think this? There is nothing wrong with having a `shared_ptr` data member if your object shares ownership of an object with another object... – James McNellis Jun 08 '12 at 17:48
  • 8
    @ViktorSehr: If putting a shared_ptr in a member variable is bad design... where else *would* you put it? How useful could shared ownership possibly be if objects can't share ownership of something? – Nicol Bolas Jun 08 '12 at 18:14
  • To clarify what the posters above have said, a shared_ptr (or often a unique_ptr or scoped_ptr if it won't be shared outside the class) is often very useful in avoiding subtle programming bugs. For example, if your constructor allocates memory for a member variable, then does something that throws an exception, the destructor never gets called, and you need to make sure to catch that exception and clean up anything that was allocated earlier. Encapsulating this in a smart pointer makes a this much easier, as that object will get cleaned up. Of course, for a string, it's unnecessary. – bdow Jun 08 '12 at 20:49
  • Smart pointers are bad ... it stops people to think about ownership. – knivil Jun 11 '12 at 07:34
  • Ew `using namespace std;`. I understand that it's just a MWE but still upsets me. – Ayberk Özgür Aug 06 '20 at 11:57

3 Answers3

19

The main issue here is not the small performance difference due to the extra atomic increment and decrement in shared_ptr but that the semantics of the operation are inconsistent unless you perform a move.

While the assumption is that the reference count of the shared_ptr will only be temporary there is no such guarantee in the language. The source object from which you are moving can be a temporary, but it could also have a much longer lifetime. It could be a named variable that has been casted to an rvalue-reference (say std::move(var)), in which case by not moving from the shared_ptr you are still maintaining shared ownership with the source of the move, and if the destination shared_ptr has a smaller scope then the lifetime of the pointed object will needlessly be extended.

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • I wonder when using moves, to what extent should we think to ourselves, "this move might degrade to a copy"? Obviously we do when writing template code for an arbitrary `MoveConstructible` type `T`, since it need not actually have a move constructor at all, let alone one that modifies the source. Also obviously, it's poor QoI if a moved-from object holds resources unnecessarily, so if `Gizmo` does have a move constructor then it should be a good one. But I think it's a matter of convention and coding style how cross we're entitled to be when it isn't. – Steve Jessop Jun 08 '12 at 17:53
  • +1, this is what I was getting at in my comment on James' answer. Well put. – ildjarn Jun 08 '12 at 17:55
  • For another example, a valid implementation of a move assignment is to call `swap`. The state of the moved-from object is unspecified, and so in particular is allowed to be the state of the moved-to object. But you'd be a bit miffed that the resources of the *moved-to* object hang around indefinitely. – Steve Jessop Jun 08 '12 at 17:59
  • 1
    @SteveJessop: There is no doubt that swapping or copying are valid implementations of *move*, but they don't conform to the *principle of least surprise*. – David Rodríguez - dribeas Jun 08 '12 at 18:31
  • +1: I've upvoted all the responses to my question, but this one raises a concern in which my code could actually break if I don't `move` the pointer. I'm going to let the question cook a little longer, as I think there are still some stones unturned. For example, are there any compiler optimization opportunities missed by not `move`ing the pointer? – John Dibling Jun 08 '12 at 18:45
  • Also, I think swap uses moves if possible in C++11 anyway. – bdow Jun 08 '12 at 20:51
  • True, it would be surprising if a type *could* be efficiently moved but *wasn't*. But take for example `char*`. If `move` sets the source to null, then it's slower than copy, which is surprising. If `move` doesn't set the source to null, then on an implementation with strict pointer safety it could prolong some resource's lifetime, just like it does on all implementations of smart pointers. Which is arguably surprising to the right person. I don't really think anyone should expect moving a raw pointer to null it, but I reckon it's possible to design classes that aren't clear-cut. – Steve Jessop Jun 10 '12 at 19:48
  • 1
    @SteveJessop: If the memory is managed by the type, the surprising thing would be not having the `char*` in the source object cleared. In general, *movable* types will hold resources by pointer, and *moving* will copy the pointer and then reset the source to 0 to relinquish ownership... – David Rodríguez - dribeas Jun 10 '12 at 21:44
  • 2
    @David: I was just pointing out that on an implementation with strict pointer safety, raw pointers might implement shared ownership (for example via a mark-sweep garbage collector). Furthermore, because of how MoveConstructible and MoveAssignable are defined in the standard, the term "movable" is probably ambiguous. It might mean "can actually be moved", or it might mean "can be moved or copied". So different people will be surprised by different things thanks to the potential for different meanings. – Steve Jessop Jun 11 '12 at 08:45
15

I upvoted James McNellis' answer. I would like to make a comment about his answer but my comment won't fit in the comment format. So I'm putting it here.

A fun way to measure the performance impact of moving a shared_ptr vs copying one is to use something like vector<shared_ptr<T>> to move or copy a whole bunch of them and time it. Most compilers have a way to turn on/off move semantics by specifying the language mode (e.g. -std=c++03 or -std=c++11).

Here is code I just tested at -O3:

#include <chrono>
#include <memory>
#include <vector>
#include <iostream>

int main()
{
    std::vector<std::shared_ptr<int> > v(10000, std::shared_ptr<int>(new int(3)));
    typedef std::chrono::high_resolution_clock Clock;
    typedef Clock::time_point time_point;
    typedef std::chrono::duration<double, std::micro> us;
    time_point t0 = Clock::now();
    v.erase(v.begin());
    time_point t1 = Clock::now();
    std::cout << us(t1-t0).count() << "\u00B5s\n";
}

Using clang/libc++ and in -std=c++03 this prints out for me:

195.368µs

Switching to -std=c++11 I get:

16.422µs

Your mileage may vary.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • 2
    +1: Just an observation. When I adapted this to use my `Gizmo` class above, the `move` and non-`move` versions times were almost identical. This was on MSVC10, using `boost::chrono` rather than `std::chrono`, compiled x64 Release. – John Dibling Jun 08 '12 at 19:46
  • @JohnDibling: Interesting. If you figure out why there is such a disparity in our results, I'd love to hear about it. One thing to try: Put a noexcept on your move constructor. I don't know whether MSVC10 implements this or not. I'd be surprised if it did considering how late noexcept came. And I actually would not expect this to make a difference for the vector::erase member. But nevertheless, it is the first thing I would try. I'm running on a 2.8 GHz Intel Core i5 (compiled for 64 bits). Were your results on the order of a few hundred microseconds, or a few tens of microseconds? – Howard Hinnant Jun 08 '12 at 21:17
13

The use of move is preferable: it should be more efficient than a copy because it does not require the extra atomic increment and decrement of the reference count.

James McNellis
  • 348,265
  • 75
  • 913
  • 977
  • There are semantic differences as well: would one expect a moved-from `Gizmo` to keep its internal shared state alive? Personally I would find that surprising, and would expect a moved-from object to release all shared state. – ildjarn Jun 08 '12 at 17:37
  • 1
    @ildjarn: I agree, though it will not affect the correctness of the program either way: the moved from object will still be destructable and assignable-to. – James McNellis Jun 08 '12 at 17:41