Skip to content

Smart Pointers: Part 2

Nathan Crookston edited this page Aug 15, 2014 · 1 revision

####Exercise: The following code opens a handle to a file using the C-style FILE* interface and closes the handle before returning.

  FILE* pFin = NULL;
  try
  {
    std::string filename("c:\\Windows\\win.ini");
    pFin = std::fopen(filename.c_str(), "r");
    if (!pFin)
      return;

    char data[10];
    if (std::fread(data, 1, 10, pFin) <= 0)
    {
      std::fclose(pFin);
      return;
    }

    std::fclose(pFin);
  }
  catch (...)
  {
    if (pFin)
      std::fclose(pFin);
    throw;  //rethrow after closing file
  }

We could change it to use iostreams, but for this exercise, change the code to just use a smart pointer (std::shared_ptr) to FILE with a custom deleter, so that the fclose() call doesn't have to appear in so many places, and eliminate the try-catch block. ####Answer:

    std::string filename("c:\\Windows\\win.ini");
    std::shared_ptr<FILE> pFin(std::fopen(filename.c_str(), "r"), &std::fclose);
    if (!pFin)
      return;

    char data[10];
    if (std::fread(data, 1, 10, pFin.get()) <= 0)
    {
      return;
    }

Notice that not only do the fclose() calls go away, but also the try/catch block goes away, since there's no longer a need to call fclose() and then re-throw.


####Question: What could you do if your deleter had a declaration that didn't match the void(T*) interface that the smart pointers expect? ####Answer: Use the adapter pattern to wrap the deleter with the appropriate interface. One easy way is to wrap it in a lambda which does match that declaration.


####Bonus Question: How would you manage a resource that uses an int-type handle instead of a pointer? Could a smart pointer still do the job, and if so, how? ####Answer: Depending on how frequently that resource was used, you may wish to create a RAII-type class to store it. If it's infrequently used, you could use a smart pointer. For example:

typedef unsigned int HANDLE;//Like a Windows API identifier.
HANDLE allocate_mutex() { return /*OS-stuff here*/ static_cast<HANDLE>(std::rand()); }
void deallocate_mutex(HANDLE) {/*OS-stuff here*/ std::cout << "Called!" << std::endl; }

struct mutex_destructor
{
  typedef HANDLE pointer;//Just store an int, not a pointer.
  void operator()(HANDLE val)
  { deallocate_mutex(val); }
};
void example()
{
  std::unique_ptr<HANDLE, mutex_destructor> mutex(allocate_mutex());
  //Note that the unique_ptr size is unchanged, since mutex_destructor has no state.
  assert(sizeof(HANDLE) == sizeof(mutex));
}

####Exercise: Using custom deleters with std::unique_ptr is a little more complicated than for std::shared_ptr, because the deleter becomes part of its type (the 2nd template argument). Try rewriting the above example to use std::unique_ptr instead of std::shared_ptr. ####Answer:

void example1unique()
{
  std::string filename("c:\\Windows\\win.ini");
  auto deleter = [](FILE* f){std::fclose(f);};
  std::unique_ptr<FILE,void(FILE*)> p_fin(std::fopen(filename.c_str(), "r"),&std::fclose);
  if (!pFin)
    return;

  char data[10];
  if (std::fread(data, 1, 10, pFin.get()) <= 0)
    return;
}

####Bonus Question: Does std::unique_ptr's size increase when you use a custom deleter? If so, why? ####Answer: Sometimes. If the deleter need not be passed in as a constructor argument, then the unique_ptr size will be unchanged. If the deleter must be referenced or otherwise stored by the unique_ptr, it will increase.


####Question: Why is the weak_ptr class designed such that we have to convert the weak_ptr into a shared_ptr before using the object the weak pointer refers to? ####Answer: You must obtain a reference to the object to insure that it is not deleted while the code is accessing it.


####Question: Assuming that p_int's lifetime is managed in a separate thread of execution, what could happen if the code were structured like this:

if (!p_weak_int.expired())
{
  auto p_tmp_copy = p_weak_int.lock();
  std::cout << "value = " << *p_tmp_copy << std::endl;
}
else
  std::cout << "expired\n";

####Answer: Consider what would happen if instruction interleaving caused the pointer to be deleted between the call to expired and the call to lock -- the next line would (hopefully) crash!


####Question: In the un-threaded example above, a raw pointer would've served just as well as a std::weak_ptr. However, suppose we had 3 threads, each owning one copy of the std::shared_ptr (so its reference count is 3). Suppose each thread could terminate at any time (thus decreasing the reference count until it reaches 0 and the memory is freed). The main thread has a std::weak_ptr copy and is monitoring/reporting its value periodically.

Now what benefit(s) might using a std::weak_ptr provide over using a copy of the raw pointer?

####Answer: If we had a raw pointer we wouldn't be able to test if it still points to anything valid (the value of the pointer doesn't get changed), whereas with a weak_ptr, we can test if the pointer is still valid.


####Question:

auto p_unique = get_unique_ptr(my_lib.open_handle(),
[&](int* p_handle) { my_lib.close_handle(p_handle); });

//Now suppose we need to change to a shared_ptr:
std::shared_ptr<int> p_shared(p_unique.release());

What's wrong with the conversion to shared_ptr and why does it cause a crash? ####Answer: The custom deleter is not transferred from the unique_ptr to the shared_ptr when you use release(). Therefore, when the shared_ptr goes out of scope, it calls delete on the raw pointer instead of the custom deleter.


####Question: The fix for the above looks like this:

std::shared_ptr<int> p_shared(std::move(p_unique));

Rewrite the code above. Why do you have to use std::move()? ####Answer: unique_ptr cannot be copied, as that would not provide unique ownership of the pointer. unique_ptr can be moved from. This allows unique_ptr to be returned from functions, used in std containers, and more. std::move explicitly moves the pointer out of p_unique into p_shared.


####Question: When we copied a shared_ptr to another shared_ptr, we didn't have to use std::move(). Why not? ####Answer: Because shared_ptrs are copy assignable whereas unique_ptrs are not. With a unique_ptr, we have to help transfer ownership through the move because ownership can not be copied.

It seems that explicitly calling std::move is usually only necessary when writing move constructors and move assignment operators. (There are other cases, like when writing algorithms, but moves are usually safest (and fastest!) when implicitly generated.)


####Question:

//We have a unique_ptr to const and we want to switch to a unique_ptr to
// non-const:
std::unique_ptr<const int> p_const_unique(new int(5));
//std::unique_ptr<int> p_my_non_const_int(std::move(p_const_unique));

If you uncomment the line above, it won't compile. Why not? ####Answer: Just as with raw pointers, converting a pointer to const to a pointer to non-const will fail.

From a higher-level point of view, if this conversion were allowed, then the following could happen.

std::unique_ptr<const int> p_const_unique(new int(5));
std::unique_ptr<int> p_my_non_const_int(std::move(p_const_unique));
// We are changing the value of something originally declared const!
*p_my_non_const_int = 4;

####Question:

//We have a shared_ptr and we want to convert it to a unique_ptr:
std::shared_ptr<int> p_shared_int(new int(6));
//std::unique_ptr<int> p_unique_int(std::move(p_shared_int));

If you uncomment the line above, it won't compile. Why not? You can create a shared_ptr from a unique_ptr, so why can't you create a unique_ptr from a shared_ptr? ####Answer: Simple answer: It would require that all others who have a shared_ptr to a resource to give it up, which isn't realistically possible in C++. Less-simple answer: While shared_ptr has a unique member function which indicates if more than one copy of it exists, it seems the committee didn't find once-shared-but-now-unique a common enough use-case to merit a conversion. Since you can check uniqueness at runtime, nothing stops you from acting as if the shared_ptr were a unique_ptr once you've checked.

Clone this wiki locally