C++ - Passing References to Std::Shared_Ptr or Boost::Shared_Ptr

C++ - passing references to std::shared_ptr or boost::shared_ptr

The point of a distinct shared_ptr instance is to guarantee (as far as possible) that as long as this shared_ptr is in scope, the object it points to will still exist, because its reference count will be at least 1.

Class::only_work_with_sp(boost::shared_ptr<foo> sp)
{
// sp points to an object that cannot be destroyed during this function
}

So by using a reference to a shared_ptr, you disable that guarantee. So in your second case:

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{
...
sp->do_something();
...
}

How do you know that sp->do_something() will not blow up due to a null pointer?

It all depends what is in those '...' sections of the code. What if you call something during the first '...' that has the side-effect (somewhere in another part of the code) of clearing a shared_ptr to that same object? And what if it happens to be the only remaining distinct shared_ptr to that object? Bye bye object, just where you're about to try and use it.

So there are two ways to answer that question:

  1. Examine the source of your entire program very carefully until you are sure the object won't die during the function body.

  2. Change the parameter back to be a distinct object instead of a reference.

General bit of advice that applies here: don't bother making risky changes to your code for the sake of performance until you've timed your product in a realistic situation in a profiler and conclusively measured that the change you want to make will make a significant difference to performance.

Update for commenter JQ

Here's a contrived example. It's deliberately simple, so the mistake will be obvious. In real examples, the mistake is not so obvious because it is hidden in layers of real detail.

We have a function that will send a message somewhere. It may be a large message so rather than using a std::string that likely gets copied as it is passed around to multiple places, we use a shared_ptr to a string:

void send_message(std::shared_ptr<std::string> msg)
{
std::cout << (*msg.get()) << std::endl;
}

(We just "send" it to the console for this example).

Now we want to add a facility to remember the previous message. We want the following behaviour: a variable must exist that contains the most recently sent message, but while a message is currently being sent then there must be no previous message (the variable should be reset before sending). So we declare the new variable:

std::shared_ptr<std::string> previous_message;

Then we amend our function according to the rules we specified:

void send_message(std::shared_ptr<std::string> msg)
{
previous_message = 0;
std::cout << *msg << std::endl;
previous_message = msg;
}

So, before we start sending we discard the current previous message, and then after the send is complete we can store the new previous message. All good. Here's some test code:

send_message(std::shared_ptr<std::string>(new std::string("Hi")));
send_message(previous_message);

And as expected, this prints Hi! twice.

Now along comes Mr Maintainer, who looks at the code and thinks: Hey, that parameter to send_message is a shared_ptr:

void send_message(std::shared_ptr<std::string> msg)

Obviously that can be changed to:

void send_message(const std::shared_ptr<std::string> &msg)

Think of the performance enhancement this will bring! (Never mind that we're about to send a typically large message over some channel, so the performance enhancement will be so small as to be unmeasureable).

But the real problem is that now the test code will exhibit undefined behaviour (in Visual C++ 2010 debug builds, it crashes).

Mr Maintainer is surprised by this, but adds a defensive check to send_message in an attempt to stop the problem happening:

void send_message(const std::shared_ptr<std::string> &msg)
{
if (msg == 0)
return;

But of course it still goes ahead and crashes, because msg is never null when send_message is called.

As I say, with all the code so close together in a trivial example, it's easy to find the mistake. But in real programs, with more complex relationships between mutable objects that hold pointers to each other, it is easy to make the mistake, and hard to construct the necessary test cases to detect the mistake.

The easy solution, where you want a function to be able to rely on a shared_ptr continuing to be non-null throughout, is for the function to allocate its own true shared_ptr, rather than relying on a reference to an existing shared_ptr.

The downside is that copied a shared_ptr is not free: even "lock-free" implementations have to use an interlocked operation to honour threading guarantees. So there may be situations where a program can be significantly sped up by changing a shared_ptr into a shared_ptr &. But it this is not a change that can be safely made to all programs. It changes the logical meaning of the program.

Note that a similar bug would occur if we used std::string throughout instead of std::shared_ptr<std::string>, and instead of:

previous_message = 0;

to clear the message, we said:

previous_message.clear();

Then the symptom would be the accidental sending of an empty message, instead of undefined behaviour. The cost of an extra copy of a very large string may be a lot more significant than the cost of copying a shared_ptr, so the trade-off may be different.

Should we pass a shared_ptr by reference or by value?

This question has been discussed and answered by Scott, Andrei and Herb during Ask Us Anything session at C++ and Beyond 2011. Watch from 4:34 on shared_ptr performance and correctness.

Shortly, there is no reason to pass by value, unless the goal is to share ownership of an object (eg. between different data structures, or between different threads).

Unless you can move-optimise it as explained by Scott Meyers in the talk video linked above, but that is related to actual version of C++ you can use.

A major update to this discussion has happened during GoingNative 2012 conference's Interactive Panel: Ask Us Anything! which is worth watching, especially from 22:50.

Should I pass a shared_ptr by reference?

In controlled circumstances you can pass the shared pointer by constant reference. Be sure that nobody is concurrently deleting the object, though this shouldn't be too hard if you're careful about to whom you give references.

In general, you should pass the shared pointer as a straight copy. This gives it its intended semantics: Every scope that contains a copy of the shared pointer keeps the object alive by virtue of its "share" in the ownership.

The only reason not to always pass by value is that copying a shared pointer comes at a certain price on account of the atomic reference count update; however, this might not be a major concern.


Optional digression:

Since the main question has been answered, perhaps it is instructive to consider a few ways in which you should never use a shared pointer. Here is a little thought experiment. Let us define a shared pointer type SF = std::shared_ptr<Foo>. In order to consider references, rather than passing function arguments let us look at the type RSF = std::reference_wrapper<T>. That is, if we have a shared pointer SF p(std::make_shared<Foo>());, then we can make a reference wrapper with value semantics via RSF w = std::ref(p);. So much for the setup.

Now, everybody knows that containers of pointers are minefield. So std::vector<Foo*> will be a nightmare to maintain, and any number of bugs arise from improper lifetime management. What's worse conceptually is that it is never clear who owns the objects whose pointers the container stores. The pointers could even be a mix of pointers to dynamic objects, automatic objects, and garbage. Nobody can tell. So the standard solution is to use std::vector<SF> instead. This is The Right Way to use the shared pointer. On the other hand, what you must never use is std::vector<RSF> -- this is an unmanageable monster that is actually very similar to the original vector of naked pointers! For example, it's not clear whether the object to which you hold a reference is still alive. Taking a reference of the shared pointer has defeated its entire purpose.

For a second example, suppose we have a shared pointer SF p as before. Now we have a function int foo(SF) that we want to run concurrently. The usual std::thread(foo, p) works just fine, since the thread constructor makes a copy of its arguments. However, had we said std::thread(foo, std::ref(p)), we'd be in all sorts of trouble: The shared pointer in the calling scope could expire and destroy the object, and you would be left with a dangling reference and an invalid pointer!

I hope these two admittedly fairly contrived examples shed a bit of light on when you really want your shared pointers to be passed around by copy. In a well-designed program, it should always be clear who is responsible for which resources, and when used right, the shared pointer is a great tool for the job.

Passing std::shared_ptr by value or const ref and then storing in a container?

In foo1 you pass the parameter (i.e., shared pointer) by value. Thus, the copy constructor of std::shared_ptr<X> is going to be evoked (i.e., ref counter is going to be increased and then decreased when destructor of local copy is being called at } of foo1).

In foo2 you pass the parameter (i.e., shared pointer) by const reference. Thus, you pass a const qualified alias of the original object (i.e., ref counter is not going to be increased).

You can also see this in the following example:

struct X {};

void foo1(std::shared_ptr<X> x) {
std::cout << "count in foo1(): " << x.use_count() << std::endl;
}

void foo2(const std::shared_ptr<X>& x) {
std::cout << "count in foo2(): " << x.use_count() << std::endl;
}

int main() {
std::shared_ptr<X> x(new X);
std::cout << "count in main(): " << x.use_count() << std::endl;
foo1(x);
foo2(x);
}

Output:

count in main(): 1
count in foo1(): 2
count in foo2(): 1

As you can see in foo1 the number of different shared_ptr instances is 2. That is the original shared_ptr defined in main and the copy in foo1. Whereas, in foo2 the ref counter remains 1.

Consequently, your reasoning is correct.

Should I switch from using boost::shared_ptr to std::shared_ptr?

There are a couple of reasons to switch over to std::shared_ptr:

  • You remove a dependency on Boost.
  • Debuggers. Depending on your compiler and debugger, the debugger may be "smart" about std::shared_ptr and show the pointed to object directly, where it wouldn't for say, boost's implementation. At least in Visual Studio, std::shared_ptr looks like a plain pointer in the debugger, while boost::shared_ptr exposes a bunch of boost's innards.
  • Other new features defined in your linked question.
  • You get an implementation which is almost guaranteed to be move-semantics enabled, which might save a few refcount modifications. (Theoretically -- I've not tested this myself) As of 2014-07-22 at least, boost::shared_ptr understands move semantics.
  • std::shared_ptr correctly uses delete [] on array types, while boost::shared_ptr causes undefined behavior in such cases (you must use shared_array or a custom deleter) (Actually I stand corrected. See this -- the specialization for this is for unique_ptr only, not shared_ptr.)

And one major glaring reason not to:

  • You'd be limiting yourself to C++11 compiler and standard library implementations.

Finally, you don't really have to choose. (And if you're targeting a specific compiler series (e.g. MSVC and GCC), you could easily extend this to use std::tr1::shared_ptr when available. Unfortunately there doesn't seem to be a standard way to detect TR1 support)

#if __cplusplus > 199711L
#include <memory>
namespace MyProject
{
using std::shared_ptr;
}
#else
#include <boost/shared_ptr.hpp>
namespace MyProject
{
using boost::shared_ptr;
}
#endif

Using boost::function with a parameter to shared pointer to derived class

There's no co-variance for function prototypes. Different signatures are simply that: different types.

In this case you'd need to wrap the function with a converting wrapper.

Let's create a few facility definitions:

using result_t = int;
struct A { };
struct B : A { };

typedef boost::shared_ptr<A> APtr;
typedef boost::shared_ptr<B> BPtr;

result_t f1(APtr) { return 1; }
result_t f2(BPtr) { return 2; }

typedef boost::function <result_t(APtr const&)> funOfA;
typedef boost::function <result_t(BPtr const&)> funOfB;

Now wrapping funOfB as a funOfA would look like this:

funOfA wrapFunOfB(const funOfB f) {
struct {
funOfB _f;
result_t operator()(APtr const& a) const {
return _f(boost::static_pointer_cast<B>(a));
}
} wrap { f };

return wrap;
}

Now you can easily write:

int main() {
F(f1);
F(wrapFunOfB(f2));
}

Simple C++03 Demo

Live On Coliru

#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>
#include <boost/function.hpp>
#include <iostream>

typedef int result_t;
struct A { int i; };
struct B : A { int j; };

typedef boost::shared_ptr<A> APtr;
typedef boost::shared_ptr<B> BPtr;

result_t f1(APtr) { return 1; }
result_t f2(BPtr) { return 2; }

typedef boost::function <result_t(APtr const&)> funOfA;
typedef boost::function <result_t(BPtr const&)> funOfB;

struct Wrapper {
typedef result_t result_type;
funOfB _f;

result_t operator()(APtr const& a) {
return _f(boost::static_pointer_cast<B>(a));
}
};

funOfA wrapFunOfB(const funOfB f) {
Wrapper wrap = { f };
return wrap;
}

void F(const funOfA f) {
APtr a = boost::make_shared<A>();
APtr b = boost::make_shared<B>();

//std::cout << "f(a): " << f(a) << "\n"; // UNDEFINED BEHAVIOUR if f wraps a funOfB
std::cout << "f(b): " << f(b) << "\n";
}

int main() {
F(f1);
F(wrapFunOfB(f2));
}

Prints

f(b): 1
f(b): 2

PROBLEMS, WARNINGS: dynamic_pointer_cast<>

If F actually invokes the parameter on an object that isn't actually of type B, that static_cast<> will invoke Undefined Behaviour.

If you want to protect against that, use dynamic_pointer_cast, which requires the classes A and B to be polymorphic types.

Live On Coliru

#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>
#include <boost/function.hpp>
#include <iostream>

typedef int result_t;
struct A { int i; virtual ~A() {} };
struct B : A { int j; };

typedef boost::shared_ptr<A> APtr;
typedef boost::shared_ptr<B> BPtr;

result_t f1(APtr a) { return a?1 : 0; }
result_t f2(BPtr b) { return b?2 : -99; }

typedef boost::function <result_t(APtr const&)> funOfA;
typedef boost::function <result_t(BPtr const&)> funOfB;

struct Wrapper {
typedef result_t result_type;
funOfB _f;

result_t operator()(APtr const& a) {
return _f(boost::dynamic_pointer_cast<B>(a));
}
};

funOfA wrapFunOfB(const funOfB f) {
Wrapper wrap = { f };
return wrap;
}

void F(const funOfA f) {
APtr a = boost::make_shared<A>();
APtr b = boost::make_shared<B>();

std::cout << "f(a): " << f(a) << "\n";
std::cout << "f(b): " << f(b) << "\n";
}

int main() {
F(f1);
F(wrapFunOfB(f2));
}

Prints

f(a): 1
f(b): 1
f(a): -99
f(b): 2

C++11 Version

Things get a little more elegant here. Notably, the Wrapper class can be local, and anonymous:

funOfA wrapFunOfB(const funOfB f) {
struct {
typedef result_t result_type;
funOfB _f;

result_t operator()(APtr const& a) {
return _f(std::dynamic_pointer_cast<B>(a));
}
} wrap { f };
return wrap;
}

Next level: use a lambda instead:

funOfA wrapFunOfB(const funOfB f) {
return [f](APtr const& a) { return f(std::dynamic_pointer_cast<B>(a)); };
}

Live On Coliru

#include <memory>
#include <functional>
#include <iostream>

typedef int result_t;
struct A { int i; virtual ~A() {} };
struct B : A { int j; };

typedef std::shared_ptr<A> APtr;
typedef std::shared_ptr<B> BPtr;

result_t f1(APtr a) { return a?1 : 0; }
result_t f2(BPtr b) { return b?2 : -99; }

typedef std::function<result_t(APtr const&)> funOfA;
typedef std::function<result_t(BPtr const&)> funOfB;

funOfA wrapFunOfB(const funOfB f) {
return [f](APtr const& a) { return f(std::dynamic_pointer_cast<B>(a)); };
}

void F(const funOfA f) {
APtr a = std::make_shared<A>();
APtr b = std::make_shared<B>();

std::cout << "f(a): " << f(a) << "\n";
std::cout << "f(b): " << f(b) << "\n";
}

int main() {
F(f1);
F(wrapFunOfB(f2));
}

That's 25% code reduction.



Related Topics



Leave a reply



Submit