Skip to content

Less known shared_ptr capabilities

Less known shared_ptr capabilities that may come in handy.

The beginnings

I’ll start out with some history. The first proposal (that I’m aware of) to include smart pointers dates back to 1994 [1] and describes two of them: auto_ptr and counted_ptr. The first one fell out of favor some time after standardization of C++98 because of unsafe move-on-copy semantics and other issues [2]. Ironically, in the first revision of this proposal copy constructor and assignment operator of  auto_ptr were private, just to be made public in further revisions. counted_ptr, on the other hand, wasn’t accepted and didn’t make it into C++98.

auto_ptr wasn’t sufficient for all conceivable smart pointer scenarios, obviously. Thus, many more smart pointer implementations appeared along the way. The most notable implementations have been introduced by the well-known Boost library and the Loki library authored by non-other than Andrei Alexandrescu. Boost introduced a couple of smart pointers, each having its own purpose. Among them was boost::shared_ptr, which eventually was included into the C++ standard [3]. Loki’s design [4] is completely different: there is only one Loki::SmartPtr. Its behavior can be controlled by choosing appropriate template parameters, that is classes, which implement desired policies of a smart pointer. In this article, I’ll concentrate on a std::shared_ptr but I encourage you to check Loki’s approach.

Since C++11, raw owning pointers are frown upon. Most C++ savvy people use smart pointers from the standard for that purpose. Many of them, though, don’t realize how advanced those smart pointers are. With this article, I’ll try to show less known std::shared_ptr powers and “tricks”.

Templated constructor

One of the often overlooked facts about std::shared_ptr is that its constructor is templated on the type of object that is being managed. Here, take a look:

template<class T> 
class shared_ptr {
  [...]
  template<class Y>
  explicit shared_ptr(Y * ptr);
  [...]
};

This is very important from the design point of view, as it allows std::shared_ptr to call the appropriate destructor. From the user point of view, this is amazingly convenient. Take a look at this declaration:

std::shared_ptr<Base> p{ new Derived };

Let’s assume Base doesn’t have a virtual destructor. Now the question is: will the right destructor get called with the last instance of this shared pointer? Yes! The Derived type was captured in the constructor precisely for that purpose. It’s all by design and very well thought.

Now, what would you say about std::shared_ptr<void>? Is such declaration valid? Does it make any sense? As you probably guessed, yes it does. Thanks to type-erasure we can declare it this way. One of the examples where such declaration might be useful is when we want to keep a reference to a shared pointer of any type or pair otherwise unrelated pointers with some data in an associative container[5].

Casts

In C++ Class<Base> is not a base of Class<Derived> even though we might want it to be. We say that templates are invariant with regard to the provided types. This is unfortunate because we would naturally want std::shared_ptr<Base> to work with std::shared_ptr<Derived>. Surprisingly (?), the following code works fine:

struct A{ 
  virtual std::string hi() { return "I'm A"; } 
};
struct B : public A { 
  std::string hi() { return "I'm B"; } 
};

int main() {
  auto b = std::make_shared<B>();
  std::shared_ptr<A> a = b;
  std::cout << a->hi();
}

It prints out “I’m B” as intended. No casts needed whatsoever. This is because std::shared_ptr‘s copy constructor retains the original type along with the proper deleter.

The standard, however, comes with a full suite of cast functions created specifically for shared pointers [6]:

template<class T, class U> 
std::shared_ptr<T> static_pointer_cast(const std::shared_ptr<U> & r);
template<class T, class U> 
std::shared_ptr<T> dynamic_pointer_cast(const std::shared_ptr<U> & r);
template<class T, class U> 
std::shared_ptr<T> const_pointer_cast(const std::shared_ptr<U> & r);
template<class T, class U> 
std::shared_ptr<T> reinterpret_pointer_cast(const std::shared_ptr<U> & r); // C++17

These help in the same cases as when we need to cast usual types. One might think that these functions are not needed and the following code means the same:

std::shared_ptr<T>(static_cast<T*>(r.get()))

but what we get here is two shared pointers owning the same object with different reference counts. Ouch!

Aliasing

Another easily overlooked feature of std::shared_ptr is the aliasing constructor. It’s a bit counter-intuitive as it allows you to create a shared pointer which returns one pointer through its get() function but maintains ownership of other, possibly unrelated pointer. The constructor looks like this:

template<class Y> 
shared_ptr(const shared_ptr<Y> & r, element_type * ptr);

Let’s take a look at an example of use:

struct A {
  int n;
  float f;
};

std::shared_ptr<int> integral;
std::shared_ptr<float> floating;
{
  std::shared_ptr<A> a{ new A{42, 3.14} };
  integral = std::shared_ptr<int>(a, &a->n);
  floating = std::shared_ptr<float>(a, &a->f);
} // a goes out of scope
std::cout << "n: " << *integral << "\n"
          << "f: " << *floating << "\n";

This prints the correct values of an object owned by a, even though a went out of scope at the end of the enclosing scope. What’s very important is that shared pointers created with aliasing constructors retain the deleter provided to the shared pointer that is being aliased. Another example where aliased constructor might be useful is object factory, which creates smaller objects that can be passed along but depend on resources owned by the factory.

Custom deleters

std::shared_ptr features custom deleters. There are so many use cases for them that it’s hard to list all of them. You can implement ‘finally’ blocks with them, nested shared pointers that have complex ownership rules, pointers that reuse object instances from a cache, pointers to statically allocated objects and so on…

template<class Y, class Deleter> 
shared_ptr(Y * ptr, Deleter d);

Quoting cppreference.com “The expression d(ptr) must be well formed, have well-defined behavior and not throw any exceptions“. So basically the deleter is a function object that takes the pointer to the object managed by a shared pointer and does something sensible with it (hopefully). If you want to see good examples of this feature I again encourage you to read [5]. I’ll give just one example:

{
  std::shared_ptr<void> finally{nullptr, 
    [](void *) { std::cout << "Is this Java?\n"; }
  };
  // do some work
  [...]
} // deleter get's executed here

I’ve heard people complaining about C++ not having ‘finally’ blocks in the standard library. As you can see it’s easy to implement with custom deleters. Not that it makes any sense when we have RAII.

Atomic access to shared_ptr

What’s commonly known is that operations that only deal with the shared pointer’s control block are thread-safe. That means it’s absolutely safe to create and destroy shared pointers owning the same object in different threads. However, accessing the same shared pointer instance from different threads is not safe. Luckily, there are specializations of std::atomic... functions that deal with std::shared_ptr. Let’s give it a try:

std::shared_ptr<int> global; // accessible from multiple threads

// thread 1
auto a = std::atomic_load(&global);

// thread 2
auto b = std::make_shared<int>(1729);
std::atomic_store(&global, b);

Changing global value this way is safe and doesn’t lead to data races. These functions are going to be replaced by a std::experimental::atomic_shared_ptr with a possibly lock-free implementation in the future standard [7].

Summary

This is just a handful of examples showing how powerful std::shared_ptr is. There is more to it than just reference counting. Knowing the full potential of the shared pointer will certainly help in writing better C++. I haven’t covered many other patterns and tricks that are possible with it. If you’re interested in digging even further into this topic, please check the below list of references.

References and further reading

[1] [N0555] Exception Safe Smart Pointers

[2] [N1232] The core language auto_ptr problem

[3] A Proposal to Add General Purpose Smart Pointers to the Library Technical Report

[4] [N1681] A Proposal to Add a Policy-Based Smart Pointer Framework to the Standard Library

[5] Smart Pointer Programming Techniques

[6] std::static_pointer_cast, std::dynamic_pointer_cast, std::const_pointer_cast, std::reinterpret_pointer_cast

[7] Atomic Smart Pointers


Header photo “Arrow” by Caleb Roenigk, available under Creative Commons Attribution license.

2 Comments

  1. A minor historical correction; neither auto_ptr nor counted_ptr from Greg Colvin’s N0555 made it into C++98. The auto_ptr that did make it into C++98 was a different proposal from other people. It was one of the thankfully few times where the committee as a whole overrode the library working group.

    When Boost was being formed in 1998, the original N0555 smart pointers went into Boost more-or-less unchanged, except that the names were changed to scoped_ptr and shared_ptr, respectively. Greg Colvin and I maintained them for awhile, and then handed them off to Peter Dimov.

    Peter added many of the more advanced features, such as weak_ptr, type erasure, and make_shared. He refined boost::shared_ptr to the point where it was accepted into TR1, and then C++11.

    –Beman Dawes

    • kszatan kszatan

      Thanks for correcting me!

Leave a Reply

Your email address will not be published. Required fields are marked *