Why Would I Std::Move an Std::Shared_Ptr

Why would I std::move an std::shared_ptr?

I think that the one thing the other answers did not emphasize enough is the point of speed.

std::shared_ptr reference count is atomic. increasing or decreasing the reference count requires atomic increment or decrement. This is hundred times slower than non-atomic increment/decrement, not to mention that if we increment and decrement the same counter we wind up with the exact number, wasting a ton of time and resources in the process.

By moving the shared_ptr instead of copying it, we "steal" the atomic reference count and we nullify the other shared_ptr. "stealing" the reference count is not atomic, and it is hundred times faster than copying the shared_ptr (and causing atomic reference increment or decrement).

Do note that this technique is used purely for optimization. copying it (as you suggested) is just as fine functionality-wise.

what does std::move(const shared_ptr reference) mean?

std::move is a function which converts the argument to an rvalue reference. The function call is an xvalue expression.

When the argument is a reference to const, then the result of the conversion is an rvalue to const. If you initialise from rvalue to const, copy constructor will be used because the rvalue reference parameter to non-const of the move constructor cannot bind to rvalue reference argument to const.

I think there is also an implicit question by OP of how _p(std::move(p)) might differ from _p(p)

_p(std::move(p)) doesn't differ from _p(p) in case of const std::shared_ptr<T>.

In theory, if decltype(_p) was a type that had a constructor T(const T&&), then there would be a difference, since that constructor would be invoked by _p(std::move(p)) but not by _p(p). Such constructor would be quite unconventional, but technically well-formed. std::shared_ptr doesn't have such constructor.

Is std::move of shared_ptr thread safe?

You have a misconception about what std::move does. In fact std::move does nothing. It's just a compile time mechanism with the meaning: I no longer need this named value.

This then causes the compiler to use the value in different ways, like call a move constructor/assignment instead of copy constructor/assignment. But the std::move itself generates no code so nothing to be affected by threads.

The real question you should be asking is whether the move constructor of shared_ptr is thread safe and the answer is: No.

But the point of the shared_ptr is not to share the shared_ptr but to share what it points too. Do not pass a shared_ptr by reference to threads, instead pass it by value so each thread gets it own shared_ptr. Then it is free to move that around at will without problems.

The shared_ptr protects the lifetime of the thing it points to. The reference count the shared_ptr uses is thread safe but nothing else. It only manages the lifetime of the pointed to objects in a thread safe way so it doesn't get destroyed as long as any shared_ptr has hold of the object.

Using std::move with std::shared_ptr

Yes, if you move the shared pointer into the function, then:

  1. the original sourcePtr will become null, and

  2. the reference count does not get modified.

If you know that you will no longer need the value of sourcePtr after the function call, moving it into the function is a slight optimisation, as it saves an atomic increment (and later decrement, when sourcePtr goes out of scope).

However, be careful that the identifier sourcePtr is still valid for the rest of the scope, just that it holds a null pointer. Which means the compiler will not complain if you use it after the move, but if you forget it was moved from, you'll in all likelihood dereference the null. I tend to use this "optimising" move a lot, and I've also been bitten by it a few times: more functionality is added to the function, and if you forget to undo the move, you get a nice crash.

So moving when you no longer need it is a slight optimisation coupled with a slight maintenance burden. It's up to you to weigh which is more important in your case.

The above assumes that there is code which actually uses sourcePtr between its declaration and the final call to foo (thanks to @WhozCraig for pointing it out). If there is not, you'd be much better off creating the pointer right at the call site:

foo(std::make_shared<X>(...));

This way, you save the same amount of atomic operations, and you don't have a potentially dangerous empty shared pointer lying around.

Move object from local variable to std::shared_ptr

How about

if (currentFooSetting) {
*currentFooSetting = f;
} else {
currentFooSetting = std::make_shared<Foo>(f);
}

this sort-of ensures that you have just one shared pointer, and once it is created, its value is changed on update. Alternately, if existing holders-of-the-shared-pointer should keep their values, just assign a new one:

currentFooSetting = std::make_shared<Foo>(f);

This will "fork" the views of current settings -- holders-of-a-shared-pointer keep the old one with old values, but functions that newly share the pointer get the new value.

Which makes more sense depends on your design (which is what Ted Lyngmo asks in a comment). Both code snippets assume your Foo class has suitable (copy) constructors.

Cost of copy vs move std::shared_ptr

I wrote a benchmark. On my Macbook Air it is three times faster (g++ as well as clang++ -std=c++17 -O3 -DNDEBUG). Let me know if you see problems with the benchmark.

#include <chrono>
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
using namespace std::chrono;

int COUNT = 50'000'000;

struct TimeIt
{
system_clock::time_point start;
TimeIt() {
start = system_clock::now();
}
~TimeIt() {
auto runtime = duration_cast<milliseconds>(system_clock::now()-start).count();
cout << runtime << " ms" << endl;
}

};

void benchmark_copy(const vector<shared_ptr<int>> &vec_src)
{
cout << "benchmark_copy" << endl;
vector<shared_ptr<int>> vec_dst;
vec_dst.reserve(COUNT);
TimeIt ti;
for(auto &sp : vec_src)
vec_dst.emplace_back(sp);
}
void benchmark_move(vector<shared_ptr<int>> &&vec_src)
{
cout << "benchmark_move" << endl;
vector<shared_ptr<int>> vec_dst;
vec_dst.reserve(COUNT);
TimeIt ti;
for(auto &sp : vec_src)
vec_dst.emplace_back(move(sp));

}

int main (int arg, char **argv){

vector<shared_ptr<int>> vec;
for (int i = 0; i < COUNT; ++i)
vec.emplace_back(new int);

benchmark_copy(vec);
benchmark_move(move(vec));

}

What is going on: C++ std::move on std::shared_ptr increases use_count?

What is going on, here?

On MacOS, it seems that you must explicitly enable move-sematics with -std=c++11 (or later standards)¹. Otherwise, the example happens to compile (i.e., std::shared_ptr from the related library implementation is usable) but doesn't work correctly as the required language features aren't enabled. This results in actual copies being made instead of move constructions. It would have been better if the AppleClang package didn't even allow an instantiation of std::shared_ptr when the required language features isn't enabled.

¹) Thanks to @t.niese for testing the given compiler/platform.

Why would a std::move of std::shared_ptr casue destruction

p && j = f(2);

Here, f returns a prvalue of type std::shared_ptr<A>. When you bind a reference to a prvalue it extends the lifetime of the prvalue to be that of the reference. That means that the object returned by f will have the same lifetime as j.

p && k = move(f(3));

Here, once again f returns a prvalue. std::move's parameter binds to that prvalue, extending its lifetime to the lifetime of the parameter. std::move does not return a prvalue though. It returns an xvalue. Lifetime extension does not apply to xvalues, so the object returned by f gets destroyed as soon as std::move's parameter's lifetime ends. That is, it gets destroyed when std::move returns. k then becomes a dangling reference.

Should I std::move a shared_ptr in a move constructor?

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.



Related Topics



Leave a reply



Submit