Skip to content

SmartPointersInFunctionSignatures

David Jewsbury edited this page Jan 30, 2015 · 3 revisions

#Smart pointer in function signatures

Consider using smart_ptrs in function signatures to denote memory semantics.

In older C++ code, have you ever seen something like this?

Object* CreateObject();     // warning -- caller must delete

class Foo
{
public:
  void Adopt(Object* obj);    // "adopt" semantics. "obj" will be deleted
};

In these examples, comments and the function names are used to communicate the memory semantics related to these functions. In the first case, the function CreateObject allocates new memory, and instructs the caller to free it. In the second case, the Foo object promises to delete the pointer given to Adopt (so the caller must not themselves attempt to delete it).

These cases are error prone, because the language does not enforce these rules. If a programmer doesn't read the comment (or if the function was never commented), they should easily cause a leak or double-delete.

But we now have tools to better document and enforce both cases:

std::unique_ptr<Object> CreateObject();

class Foo
{
public:
  void Adopt(std::unique_ptr<Object> obj);
};

This is great. But now we have so many different ways to write the same function!

void Function(Object&);
void Function(const Object&);
void Function(Object&&);

void Function(Object*);
void Function(const Object*);
void Function(Object*&);
void Function(Object*&&);

void Function(std::unique_ptr<Object>);
void Function(std::unique_ptr<Object>&);
void Function(const std::unique_ptr<Object>&);
void Function(std::unique_ptr<Object>&&);

void Function(std::shared_ptr<Object>);
void Function(std::shared_ptr<Object>&);
void Function(const std::shared_ptr<Object>&);
void Function(std::shared_ptr<Object>&&);

Which of these function signatures are useful, and how should we use each?

###std::shared_ptr<> problem

One problem with std::shared_ptr<>, is that we can't use raw pointers if we expect someone will need to add a new reference count to that object sometime in the future.

Consider:

  • class A has a raw pointer to an instance obj
  • class B has a std::shared_ptr<> to the same instance obj

What if class A wants to pass obj to a function that needs to increment the reference count? It can't create a new std::shared_ptr<>, because the reference count in that pointer wouldn't agree with the pointer in class B.

There is an exception when using std::enable_shared_from_this. But we can't always rely on that. What this means is, sometimes we need to pass or return a std::shared_ptr<> just because we're not really sure what the other side will do with it. Passing a raw pointer normally means "don't take any more reference counts." Passing a std::shared_ptr<> could mean "it's ok to take a reference, if you need it."

###Smart pointers as return values

Consider the following cases:

// The follow syntax should be the most common for creation functions. They
// tell the client exactly what to do, and enforce the right result:
std::unique_ptr<Object> CreationFunction();
std::shared_ptr<Object> CreationFunction();

// Frequently, returning a std::unique_ptr<> should be fine in this case. The caller
// can promote it to a std::shared_ptr<> if needed.
//
// But we may want to use a shared_ptr<> sometimes:
//   * if CreationFunction() stores a reference somewhere else
//   * if Object is derived from std::enable_shared_from_this (using a unique_ptr would work, I guess, but it's wierd)
//   * if we want to use std::make_shared<>
//   * if we're sometimes returning a new object, but other times returning a cached, shareable object

// "Get" type functions that never create can often just return raw pointer or reference:
Object* GetObject();  // result could be null (maybe on not-found)
Object& GetObject();  // result can never be null (a valid object is always returned)

// In the above cases, the caller should not attempt to use std::shared_ptr<> with the
// returned object.
// There are cases were we want to return a reference to a std::shared_ptr<>, when we
// believe that the caller may (at least sometimes) want to take a new reference:
std::shared_ptr<Object>& GetObject();

// In the above case, the caller can assign the return to a std::shared_ptr<> if they
// need a new reference. Or if they don't need a new reference, they can just use the
// object directly.
// It may seem safer to aways return a new std::shared_ptr<> by value, but that would
// invoke extra overhead when the caller doesn't need a new reference

###Smart pointers as parameters

When we use a smart pointer in a function parameter list, we're doing 2 things:

  • we're restricting the type of pointer the caller can use
  • (eg, we can't pass a std::shared_ptr<> to a function that takes a std::unique_ptr<>)
  • we're explaining to the caller what we will do with that object.

Here are most useful cases:

// Most functions don't have any special memory semantics. They neither delete their
// parameters, or take new references. In these cases, it's just like old C++:
void Function(const Object& obj); // obj must be valid, and cannot be null
void Function(Object* obj);       // this function will do something sensible if obj is null

// The following is a useful pattern:
// Use of a unique_ptr<> here makes it explicit that this function will
// take ownership of the object, and delete it sometime before application
// shutdown.
// This makes it very clear what's going to happen, and prevents the
// double-delete case.
void Function(std::unique_ptr<Object> obj);

// This pattern seems to be more sensible than the following:
void Function(std::unique_ptr<Object>& obj);    // a little weird
void Function(std::unique_ptr<Object>&& obj);   // also a little weird.

// The reference and rvalue reference version should probably be more or less
// equivalent. But passing by value seems to make more sense in this case.

// Some functions always take a new reference on objects passed in. In these
// cases, normally passing a std::shared_ptr<> should be clearest
void Function(std::shared_ptr<Object> obj);     // always takes a new reference

// Maybe we could use a const std::shared_ptr<>& if the function only sometimes
// takes a new reference (but other times doesn't change the reference count):
void Function(const std::shared_ptr<Object>& obj);

// We can also pass non-const references for in-out type functions. In these
// cases, Function may change the pointer before returning.
void Function(std::unique_ptr<Object>& obj);
void Function(std::shared_ptr<Object>& obj);

###Avoid redundant AddRef/Releases

It's worth remembering that taking new references and releasing references in std::shared_ptr<>s can be an expensive operation. This means that we should be careful about passing and returning std::shared_ptr<> by value.

In cases where there will definitely be a change in the reference count, passing by value is the cleanest. But in cases where there is doubt, then prefer using a std::shared_ptr<>&.

For more information, see the link at the bottom.

###Nullable smart pointers

It's worth noting that all smart pointers are nullable (unlike references). There are some cases described here that might have used a reference in old C++, but would now use a smart pointer. In these cases, be kind to your callers and add in an assert() to catch the nullptr case.

##See also

There's a nice write-up on a similar topic here: Herb Sutter's Blog