';

[C++ job interview Q&A] What are the smart pointers in C++?

The other popular C++ interview question is “What are the smart pointers in C++? Please, name and describe all of them”.
It’s essential to know smart pointers, as they’re an important reason we choose modern C++ instead of C or C++98. Why? It’s mainly because of all of the disadvantages of raw pointers:
1. You never know whether a raw pointer points to a single object or to an array.
2. You never know whether you’re responsible for destroying an object the raw pointer points to.
3. You may never know if the pointer still points to a valid object, i.e. the pointer dangles.
4. It’s difficult to ensure that you delete the pointer only once along every path in the code

Smart pointers are supposed to solve all these problems. Essentially, they’re just wrappers to raw pointers. In C++17, there are 3 types of smart pointers (since std::auto_ptr has been removed from the standard since C++17).

std::unique_ptr (LINK)

Std::unique_ptr is the first natural choice when replacing raw pointers with smart pointers. By default, unique_ptr is of the same size as a raw pointer, and for most operations, it’s just as efficient.
However, std::unique_ptr was designed for exclusive-ownership resource management, i.e. it owns what it points to. This means that the std::unique_ptr is also exclusively responsible for destroying an object it points to. When? Std::unique_ptr releases acquired resources in its destructor, so e.g. when going out of the scope.
It’s not allowed to copy the unique_ptr (you would end up with 2 unique_ptrs owning the same resource in such case), and it’s a move-only type.

Std::unique_ptr by default uses std::default_delete, but it can also be configured to use a custom deleter function – a function that is to be invoked on destroying the resource. However, it’s worth to notice that std::unique_ptr configured with a custom deleter no longer is of the same size as a raw pointer. Custom deleter causes the size of std::unique_ptr to grow from one word to two.

To sum up, when do we use std::unique_ptr?

  • To address the issues introduced by raw pointers.
  • To manage resources – so we’re sure that the resource is valid, and properly – and only once – freed.
  • To implement pimpl idiom – as std::unique_ptr may be constructed for an incomplete type.
  • To implement a factory function returning types for objects in the hierarchy.

The great feature of std::unique_ptr is also that it easily and efficiently converts to std::shared_ptr.

#include <memory>
class A
{
public:
  virtual ~A() = default;
};

void SimpleUse()
{
  auto p = std::make_unique<A>(); // auto of std::unique_ptr<A> type.
  // p deletes the object it points to at the end of its scope.
}

class B : public A {};

// Factory method example.
std::unique_ptr<A> FactoryMethod() const
{
  return std::make_unique<B>();
}

// Pimpl example.
class C
{
public:
  ~C(); // Non-default dtor is needed for pimpl because Impl type is incomplete in this context. The destructor can be defaulted in the implementation file.

private:
  class Impl;
  std::unique_ptr<Impl> pimpl;
};

std::shared_ptr

Paraphrasing Scott Meyers:

std::shared_ptr is the C++ way of binding two worlds together – garbage collection and manual lifetime management.

The object pointed by multiple shared_ptrs has its lifetime managed by all those pointers (it’s called shared ownership). It means that no specific shared_ptr owns the object, but all of them actually do. Last shared_ptr pointing to an object, destroys it once it stops pointing to it.
Long story short, the object is destroyed once it’s no longer used.

How an std::shared_ptr can know when to destroy the object it points to? It associates the reference count with the resource. Once the reference count becomes zero, std::shared_ptr destroys the resource.
The reference count is a part of the data structure called the control block. There’s a control block for each object pointed by std::shared_ptr.

SmartPointer - shared_ptr – StonzeBlog
std::shared_ptr internals [source: https://stonzeteam.github.io/Shared_Ptr/]

The existence of the control block has its performance implications:

  • A single std::shared_ptr is twice the size of a raw pointer.
  • The control block has to be allocated dynamically which is an expensive operation. However, you need to know that using std::make_shared() function, only one allocation takes place for both the object and control block.
  • Operations on the reference counter (incrementing and decrementing it) must be atomic because it can be read and write from different threads.

Std::shared_ptr also supports custom deleter functions. The deleter is also stored in the control block.

To sum up, std::shared_ptr shall be used in the following situations:

  • To share the resource between multiple classes.
  • To taste the garbage collection. At last…

… but, you absolutely shall avoid shared_ptr whenever unique_ptr can be used.

#include <memory>
class A 
{
public:
  virtual ~A() = default;
};
class B : public A {};

void SimpleUse()
{
  auto p = std::make_shared<A>();
  // Share the p with some object.
  // Share the p with another object.
  // ...

  // p doesn't deletes the object it points to at the end of the function scope. The object will be deleted once all other objects stop pointing it.
}

void FromUniquePtr()
{
  auto up = std::make_unique<A>();
  std::shared_ptr<A> sp = std::move(up);
}

std::weak_ptr

Weak_ptr is a non-owning (i.e. “weak”) pointer pointing to an object managed by std::shared_ptr. It doesn’t affect an object’s reference count, but it’s created from shared_ptr. Before use, it has to be checked for validity and converted back to std::shared_ptr.

An std::weak_ptr shall be used:

  • For std::shared_ptrs that can dangle.
  • To break std::shared_ptr circular references. A circular reference is a series of references where each object references the next, and the next object references back to the previous, causing a loop. In such case, 2 shared_ptrs having circular references will never go out of scope (the ref count will never be zero!). To break the loop, std::weak_ptr has to be used as it doesn’t participate in the reference count.

The efficiency of std::weak_ptr is essentially the same as of std::shared_ptr.

#include <memory>

class A { public: virtual ~A() = default; };

void ExampleUse()
{
  std::shared_ptr<A> p = std::make_shared<A>();
  std::weak_ptr<A> wp = p;
  // ...
  if (auto sp = wp.lock(); sp)
  {
    // Pointer still valid.
  }
  else
  {
    // Pointer expired.
  }
}
Recommend
Share
Tagged in
Leave a reply