Skip to content

Latest commit

 

History

History
577 lines (427 loc) · 11.3 KB

main.smartptr.cpp11.adoc

File metadata and controls

577 lines (427 loc) · 11.3 KB

C++11

Smart Pointers

smart ptr

C++11 introduces std::unique_ptr, std::shared_ptr, std::weak_ptr and deprecates std::auto_ptr.

This are for ease the management of memory and other resources.

They remove the need to write custom destructors for nearly all use cases.

Smart pointers are owning pointers

Smart pointers are "owning" pointers. They release the resource they point to.

If you do not own the resource, use a "raw" pointer (or a reference).

What is a unique_ptr

std::unique_ptr models exclusive ownership:

  • std::unique_ptr owns the raw pointer

  • there can’t be a second owner (unless misuse)

ownership can be passed to another unique_pointer by moving

Example

struct bar{ };

void foo(){
  std::unique_ptr<bar> uptr0;
  {
    auto uptr1 = std::make_unique<bar>();
    //uptr0 = uptr1; // copy is a comp-time eror
    uptr0 = std::move(uptr1); // moves pointer from uptr1 to uptr0
  }
}

unique_ptr models onwership, not only memory ownersip

The owner calls delete by default, but it is possible to use other deleters too.

struct file_deleter {
    void operator()(std::FILE* fp) { std::fclose(fp); }
};
using unique_file = std::unique_ptr<std::FILE, file_deleter>;

auto f = unique_file(std::fopen("file.txt", "rw"));

unique_ptr has been designed in a way to be easy to integrate in existing code-basis

No overhead

  • same size of a raw pointer

  • no allocations, or other operation that could fail

  • provides direct access to owned pointer

  • works with incomplete types

Behaves like a pointer

struct bar{void foo(){};};

auto pb = new bar();
auto upb = std::make_unique<bar>();

pb->foo();
upb->foo();

if(pb){}
if(pb == nullptr){}

if(upb){}
if(upb == nullptr){}

bar& b = *pb;
bar& ub = *upb;

It is possible to .release() ownership

struct bar{ };

auto uptr std::make_unique<bar>();
auto ptr = uptr.release();
// uptr does not own anything, it owns nullptr
delete ptr;

It is generally best avoided, but might be necessary.

Replacing raw owning pointer in existing code

As unique_ptr has an interface similar to a normal pointer, it is possible to replace the usage of raw pointers with unique_ptr iteratively.

During the process, most error are diagnosed by the compiler.

Replacing raw owning pointer in existing code

Generally removing the usage of raw owning pointers

  • documents better than a comment who owns the resource

  • permits the compiler to validate assumptions and diagnose errors

  • simplifies code in unexpected ways

  • avoid leaks that are easy to oversee

Assigning pointers

before

delete ptr;
ptr = new X();

// or
delete ptr;
ptr = newptr;

after

ptr = std::make_unique<X>();

// or
ptr.reset(newptr);

Clearing containers

before

for( std::vector<T*>::iterator it = vec.begin(); it != vec.end(); ++it ){
  delete *it;
}
vec.clear();

after

vec.clear();

or even

Inserting new elements in containers

before

struct foo {
  using internal_map = std::map<std::string, I*>;
  using external_map = std::map<std::string, internal_map>;
  external_map m_map;

  ~foo() {
    for ( auto it = m_map.begin(); it != m_map.end(); ++it ) {
      for ( auto subIt = it->second.begin(); subIt != it->second.end(); ++subIt ) {
        delete subIt->second;
      }
    }

  void bar(const std::string& str1, const std::string& str2, I* i) {
    auto it = m_map.find(str1);
    if ( it != m_map.end() ) {
      auto subIt = it->second.find(str2);
      if ( subIt != it->second.end() ) {
        delete subIt->second;
      }
      it->second[str2] = i;
    } else {
      internal_map imap;
      imap[str2] = i;
      m_map[str1] = imap;
    }
  }
}

  // ...
};

Inserting new elements in containers

after

struct foo {
  using internal_map = std::map<std::string, std::unique_ptr<I>>;
  using external_map = std::map<std::string, internal_map>;
  external_map m_map;

  void bar(const std::string& str1, const std::string& str2, I* i) {
    m_map[str1][str2].reset(i);
  }

  // ...
};

Rules of thumbs for avoiding leaks

Without unique_ptr or similar alternatives

  • enlist every place where a resource is allocated

  • track where handles to the resources are passed

  • verify that every resource is closed only once

  • verify that the correct function for closing the resource is used

It’s hard and error prone to apply those guidelines consistently

Rules of thumbs for avoiding leaks

C* foo();

Should we free/close the return value of foo. Do we own it? If yes, how?

Rules of thumbs for avoiding leaks

void bar(C*);
void foo(){
  auto i = new C();
  bar(i);
}

Is it correct that no delete takes place? Does it happen inside bar?

Rules of thumbs for avoiding leaks

void bar(C*);
void foo(){
  auto i = new C();
  bar(i);
  delete i;
}

Is i always released? If not, is it correct?

Rules of thumbs for avoiding leaks

void bar(C*);
void foo() {
  auto i = new C();
  try {
    bar(i);
  } catch (...) {
    delete i;
    throw;
  }
  delete i;
}

Is very verbose and error-prone.

Rules of thumbs for avoiding leaks

void bar(C*, C*);
void foo() {
  auto i = new C();
  try {
    auto j = new C();
    try {
      bar(i, j);
    } catch (...) {
      delete j;
      throw;
    }
    delete j;
  } catch (...) {
    delete i;
    throw;
  }
  delete i;
}

It does not scale, no-one writes code like that. Imagine a function or constructor with 3 parameters.

Rules of thumbs for avoiding leak

Similar difficulties exists when implementing a class that owns more than one resource

struct bar {
  C* i;
  C* j;
  explicit bar():
   i(new C()),
   j(new C()) {
  }

  ~bar(){
    delete j;
    delete i;
  }
};

Fixing bar without using helper classes is left as exercise to the reader.

Rules of thumbs for avoiding leaks

With unique_ptr or similar constructs

  • Newer use non-owning handles for owning resources

  • verify if and where .release() is used, as it is equivalent to manage resources manually

Which is ideally equivalent to

  • Do not handle resources manually

Rules of thumbs for avoiding leaks

std::unique_ptr<C> foo();
C* foo();

In both cases, from the function signature its clear if we own the returned value or not. In both cases, calling foo() does not cause any leak by design.

Rules of thumbs for avoiding leaks

void bar(std::unique_ptr<C>);
void foo(){
  auto ptr = std::make_unique<C>();
  bar(std::move(ptr));
}

or

void bar(C*);
void foo(){
  auto ptr = std::make_unique<C>();
  bar(ptr.get());
}

In both cases it’s clear if bar takes ownership of the parameter or not, there is no need to track about who owns the pointer. It also scales; the written code does not depend on the number of execution paths.

Rules of thumbs for avoiding leaks

struct bar {
  std::unique_ptr<C> i;
  std::unique_ptr<C> j;
  explicit bar() :
   i(std::make_unique<C>()) ,
   j(std::make_unique<C>()) {
  }
  // ~bar is compiler-generated
};

bar is leak-free.

As a corollary of the single-responsibility principle: A class should own directly only one resource.

Rules of thumbs for avoiding leak

.release() is mostly necessary when working with external or C libraries, as we cannot change those functions to use unique_ptr.

It is possible to wrap/hide those functions (which are a small subset of the code we control) and make them work with unique_ptr too.

Supposing that third-party code is correct, it possible to keep track of places where .release() is needed to a very limited subset of code, and ensure the absence of leaks with a grep.

example

before

auto attr_p = xmlGetProp(rNode, "p");

after

struct free_xml_generic {
  void operator()(void* handle) { xmlFree(handle); }
};
template <class T>
using unique_xmlPtr = std::unique_ptr<T, free_xml_generic>;↲
using unique_xmlChar = unique_xmlPtr<xmlChar>;↲

inline unique_xmlChar get_unique_xmlProp(const xmlNode* node, const xmlChar* name) {
  return unique_xmlChar(xmlGetProp(node, name));
}
#pragma GCC poison xmlGetProp

auto attr_p = get_unique_xmlProp(rNode, "p");

shared_ptr

Did not talk much about std::shared_ptr and std::weak_ptr because there are not as many use cases as with std::unique_ptr.

shared_ptr

It has a copy constructor, every copy increments a thread-safe counter:

  • it is more difficult, if possible, to understand who owns the resource

  • can cause leaks with circular dependencies

  • has non-trivial overhead compared to a raw pointer

  • the synchronisation is costly if not needed

  • it is trivial to convert an unique_ptr to a shared_ptr, generally impossible to do the opposite

shared_ptr

Nevertheless, there are valid use-cases (cow, garbage collection, cache, …​)

They are not less important, but in most cases, shared ownership is best avoided.

rules of thumb when using unique_ptr

Avoid passing unique_ptr by const-ref

struct bar{ };

// bad, forces the user to allocate bar on the heap,
// even if foo cannot use that information
void foo(const std::unique_ptr<bar>&);

// better
void foo(const bar*);

// even better if bar cannot/should not be null
void foo(const bar&);

// even better if bar is "small", like integral types, enums or small data structures, like string_view, span, ...
void foo(bar);

Avoid returning unique_ptr by const-ref

// bad, leaks implementation details that the caller cannot use
const std::unique_ptr<T>& foo();

// better
T* foo();

// even better if return value cannot/should not be null
const T& foo();

Pass by value to denote unconditional transfer in ownership

class X {
    std::unique_ptr<int> i;
  public:
    explicit X(std::unique_ptr<int> ii) : i(std::move(ii)) {}
};
void bar(std::unique_ptr<int>);

void foo(){
  auto i = std::make_unique<int>(42);
  X x(std::move(i));
  bar(std::move(i));

  bar(std::make_unique<int>(42));
}