11
SMART POINTERS

If you want to do a few small things right, do them yourself. If you want to do great things and make a big impact, learn to delegate.
—John C. Maxwell

Image

In this chapter, you’ll explore stdlib and Boost libraries. These libraries contain a collection of smart pointers, which manage dynamic objects with the RAII paradigm you learned in Chapter 4. They also facilitate the most powerful resource management model in any programming language. Because some smart pointers use allocators to customize dynamic memory allocation, the chapter also outlines how to provide a user-defined allocator.

Smart Pointers

Dynamic objects have the most flexible lifetimes. With great flexibility comes great responsibility, so you must make sure each dynamic object gets destructed exactly once. This might not look daunting with small programs, but looks can be deceiving. Just consider how exceptions factor into dynamic memory management. Each time an error or an exception could occur, you need to keep track of which allocations you’ve made successfully and be sure to release them in the correct order.

Fortunately, you can use RAII to handle such tedium. By acquiring dynamic storage in the constructor of the RAII object and releasing dynamic storage in the destructor, it’s relatively difficult to leak (or double free) dynamic memory. This enables you to manage dynamic object lifetimes using move and copy semantics.

You could write these RAII objects yourself, but you can also use some excellent prewritten implementations called smart pointers. Smart pointers are class templates that behave like pointers and implement RAII for dynamic objects.

This section delves into five available options included in stdlib and Boost: scoped, unique, shared, weak, and intrusive pointers. Their ownership models differentiate these five smart pointer categories.

Smart Pointer Ownership

Every smart pointer has an ownership model that specifies its relationship with a dynamically allocated object. When a smart pointer owns an object, the smart pointer’s lifetime is guaranteed to be at least as long as the object’s. Put another way, when you use a smart pointer, you can rest assured that the pointed-to object is alive and that the pointed-to object won’t leak. The smart pointer manages the object it owns, so you can’t forget to destroy it thanks to RAII.

When considering which smart pointer to use, your ownership requirements drive your choice.

Scoped Pointers

A scoped pointer expresses non-transferable, exclusive ownership over a single dynamic object. Non-transferable means that the scoped pointers cannot be moved from one scope to another. Exclusive ownership means that they can’t be copied, so no other smart pointers can have ownership of a scoped pointer’s dynamic object. (Recall from “Memory Management” on page 90 that an object’s scope is where it’s visible to the program.)

The boost::scoped_ptr is defined in the <boost/smart_ptr/scoped_ptr.hpp> header.

NOTE

There is no stdlib scoped pointer.

Constructing

The boost::scoped_ptr takes a single template parameter corresponding to the pointed-to type, as in boost::scoped_ptr<int> for a “scoped pointer to int” type.

All smart pointers, including scoped pointers, have two modes: empty and full. An empty smart pointer owns no object and is roughly analogous to a nullptr. When a smart pointer is default constructed, it begins life empty.

The scoped pointer provides a constructor taking a raw pointer. (The pointed-to type must match the template parameter.) This creates a full-scoped pointer. The usual idiom is to create a dynamic object with new and pass the result to the constructor, like this:

boost::scoped_ptr<PointedToType> my_ptr{ new PointedToType };

This line dynamically allocates a PointedToType and passes its pointer to the scoped pointer constructor.

Bring in the Oath Breakers

To explore scoped pointers, let’s create a Catch unit-test suite and a DeadMenOfDunharrow class that keeps track of how many objects are alive, as shown in Listing 11-1.

#define CATCH_CONFIG_MAIN 
#include "catch.hpp" 
#include <boost/smart_ptr/scoped_ptr.hpp> 

struct DeadMenOfDunharrow { 
  DeadMenOfDunharrow(const char* m="") 
    : message{ m } {
    oaths_to_fulfill++; 
  }
  ~DeadMenOfDunharrow() {
    oaths_to_fulfill--; 
  }
  const char* message;
  static int oaths_to_fulfill;
};
int DeadMenOfDunharrow::oaths_to_fulfill{};
using ScopedOathbreakers = boost::scoped_ptr<DeadMenOfDunharrow>; 

Listing 11-1: Setting up a Catch unit-test suite with a DeadMenOfDunharrow class to investigate scoped pointers

First, you declare CATCH_CONFIG_MAIN so Catch will provide an entry point and include the Catch header and then the Boost scoped pointer’s header . Next, you declare the DeadMenOfDunharrow class ,which takes an optional null-terminated string that you save into the message field . The static int field called oaths_to_fulfill tracks how many DeadMenOfDunharrow objects have been constructed. Accordingly, you increment in the constructor , and you decrement in the destructor . Finally, you declare the ScopedOathbreakers type alias for convenience .

CATCH LISTINGS

You’ll use Catch unit tests in most listings from now on. For conciseness, the listings omit the following Catch ceremony:

#define CATCH_CONFIG_MAIN
#include "catch.hpp"

All listings containing TEST_CASE require this preamble.

Also, every test case in each listing passes unless a comment indicates otherwise. Again, for conciseness, the listings omit the All tests pass output from the listings.

Finally, tests that employ user-defined types, functions, and variables from a previous listing will omit them for brevity.

Implicit bool Conversion Based on Ownership

Sometimes you need to determine whether a scoped pointer owns an object or whether it’s empty. Conveniently, scoped_ptr casts implicitly to bool depending on its ownership status: true if it owns an object; false otherwise. Listing 11-2 illustrates how this implicit casting behavior works.

TEST_CASE("ScopedPtr evaluates to") {
  SECTION("true when full") {
    ScopedOathbreakers aragorn{ new DeadMenOfDunharrow{} }; 
    REQUIRE(aragorn); 
  }
  SECTION("false when empty") {
    ScopedOathbreakers aragorn; 
    REQUIRE_FALSE(aragorn); 
  }
}

Listing 11-2: The boost::scoped_ptr casts implicitly to bool.

When you use the constructor taking a pointer , the scoped_ptr converts to true . When you use the default constructor , the scoped_ptr converts to false.

RAII Wrapper

When a scoped_ptr owns a dynamic object, it ensures proper dynamic object management. In the scoped_ptr destructor, it checks whether it owns an object. If it does, the scoped_ptr destructor deletes the dynamic object.

Listing 11-3 illustrates this behavior by investigating the static oaths_to_fulfill variable between scoped pointer initializations.

TEST_CASE("ScopedPtr is an RAII wrapper.") {
  REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 0); 
  ScopedOathbreakers aragorn{ new DeadMenOfDunharrow{} }; 
  REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 1); 
  {
    ScopedOathbreakers legolas{ new DeadMenOfDunharrow{} }; 
    REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 2); 
  } 
  REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 1); 
}

Listing 11-3: The boost::scoped_ptr is an RAII wrapper.

At the beginning of the test, oaths_to_fulfill is 0 because you haven’t constructed any DeadMenOfDunharrow yet . You construct the scoped pointer aragorn and pass in a pointer to the dynamic DeadMenOfDunharrow object . This increments the oaths_to_fulfill to 1 . Within a nested scope, you declare another scoped pointer legolas . Because aragorn is still alive, oaths_to_fulfill is now 2 . Once the inner scope closes, legolas falls out of scope and destructs, taking a DeadMenOfDunharrow with it . This decrements DeadMenOfDunharrow to 1 .

Pointer Semantics

For convenience, scoped_ptr implements the dereference operator* and the member dereference operator->, which simply delegate the calls to the owned dynamic object. You can even extract a raw pointer from a scoped_ptr with the get method, as demonstrated in Listing 11-4.

TEST_CASE("ScopedPtr supports pointer semantics, like") {
  auto message = "The way is shut";
  ScopedOathbreakers aragorn{ new DeadMenOfDunharrow{ message } }; 
  SECTION("operator*") {
    REQUIRE((*aragorn).message == message); 
  }
  SECTION("operator->") {
    REQUIRE(aragorn->message == message); 
  }
  SECTION("get(), which returns a raw pointer") {
    REQUIRE(aragorn.get() != nullptr); 
  }
}

Listing 11-4: The boost::scoped_ptr supports pointer semantics.

You construct the scoped pointer aragorn with a message of The way is shut , which you use in three separate scenarios to test pointer semantics. First, you can use operator* to dereference the underlying, pointed-to dynamic object. In the example, you dereference aragorn and extract the message to verify that it matches . You can also use operator-> to perform member dereference . Finally, if you want a raw pointer to the dynamic object, you can use the get method to extract it .

Comparison with nullptr

The scoped_ptr class template implements the comparison operators operator== and operator!=, which are only defined when comparing a scoped_ptr with a nullptr. Functionally, this is essentially identical to implicit bool conversion, as Listing 11-5 illustrates.

TEST_CASE("ScopedPtr supports comparison with nullptr") {
  SECTION("operator==") {
    ScopedOathbreakers legolas{};
    REQUIRE(legolas == nullptr); 
  }
  SECTION("operator!=") {
    ScopedOathbreakers aragorn{ new DeadMenOfDunharrow{} };
    REQUIRE(aragorn != nullptr); 
  }
}

Listing 11-5: The boost::scoped_ptr supports comparison with nullptr.

An empty scoped pointer equals (==) nullptr , whereas a full scoped pointer doesn’t equal (!=) nullptr .

Swapping

Sometimes you want to switch the dynamic object owned by a scoped_ptr with the dynamic object owned by another scoped_ptr. This is called an object swap, and scoped_ptr contains a swap method that implements this behavior, as shown in Listing 11-6.

TEST_CASE("ScopedPtr supports swap") {
  auto message1 = "The way is shut.";
  auto message2 = "Until the time comes.";
  ScopedOathbreakers aragorn {
    new DeadMenOfDunharrow{ message1 } 
  };
  ScopedOathbreakers legolas {
    new DeadMenOfDunharrow{ message2 } 
  };
  aragorn.swap(legolas); 
  REQUIRE(legolas->message == message1); 
  REQUIRE(aragorn->message == message2); 
}

Listing 11-6: The boost::scoped_ptr supports swap.

You construct two scoped_ptr objects, aragorn and legolas , each with a different message. After you perform a swap between aragorn and legolas , they exchange dynamic objects. When you pull out their messages after the swap, you find that they’ve switched .

Resetting and Replacing a scoped_ptr

Rarely do you want to destruct an object owned by scoped_ptr before the scoped_ptr dies. For example, you might want to replace its owned object with a new dynamic object. You can handle both of these tasks with the overloaded reset method of scoped_ptr.

If you provide no argument, reset simply destroys the owned object.

If you instead provide a new dynamic object as a parameter, reset will first destroy the currently owned object and then gain ownership of the parameter. Listing 11-7 illustrates such behavior with one test for each scenario.

TEST_CASE("ScopedPtr reset") {
  ScopedOathbreakers aragorn{ new DeadMenOfDunharrow{} }; 
  SECTION("destructs owned object.") {
    aragorn.reset(); 
    REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 0); 
  }
  SECTION("can replace an owned object.") {
    auto message = "It was made by those who are Dead.";
    auto new_dead_men = new DeadMenOfDunharrow{ message }; 
    REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 2); 
    aragorn.reset(new_dead_men); 
    REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 1); 
    REQUIRE(aragorn->message == new_dead_men->message); 
    REQUIRE(aragorn.get() == new_dead_men); 
  }
}

Listing 11-7: The boost::scoped_ptr supports reset.

The first step in both tests is to construct the scoped pointer aragorn owning a DeadMenOfDunharrow . In the first test, you call reset without an argument . This causes the scoped pointer to destruct its owned object, and oaths_to_fulfill decrements to 0 .

In the second test, you create the new, dynamically allocated new_dead_men with a custom message . This increases the oaths_to_fill to 2, because aragorn is also still alive . Next, you invoke reset with new_dead_men as the argument , which does two things:

  • It causes the original DeadMenOfDunharrow owned by aragorn to get destructed, which decrements oaths_to_fulfill to 1 .
  • It emplaces new_dead_men as the dynamically allocated object owned by aragorn. When you dereference the message field, notice that it matches the message held by new_dead_men . (Equivalently, aragorn.get() yields new_dead_men .)

Non-transferability

You cannot move or copy a scoped_ptr, making it non-transferable. Listing 11-8 illustrates how attempting to move or copy a scoped_ptr results in an invalid program.

void by_ref(const ScopedOathbreakers&) { } 
void by_val(ScopedOathbreakers) { } 

TEST_CASE("ScopedPtr can") {
  ScopedOathbreakers aragorn{ new DeadMenOfDunharrow };
  SECTION("be passed by reference") {
    by_ref(aragorn); 
  }
  SECTION("not be copied") {
    // DOES NOT COMPILE:
    by_val(aragorn); 
    auto son_of_arathorn = aragorn; 
  }
  SECTION("not be moved") {
    // DOES NOT COMPILE:
    by_val(std::move(aragorn)); 
    auto son_of_arathorn = std::move(aragorn); 
  }
}

Listing 11-8: The boost::scoped_ptr is non-transferable. (This code doesn’t compile.)

First, you declare dummy functions that take a scoped_ptr by reference and by value . You can still pass a scoped_ptr by reference , but attempting to pass one by value will fail to compile . Also, attempting to use the scoped_ptr copy constructor or a copy assignment operator will fail to compile. In addition, if you try to move a scoped_ptr with std::move, your code won’t compile .

NOTE

Generally, using a boost::scoped_ptr incurs no overhead compared with using a raw pointer.

boost::scoped_array

The boost::scoped_array is a scoped pointer for dynamic arrays. It supports the same usages as a boost::scoped_ptr, but it also implements an operator[] so you can interact with elements of the scoped array in the same way as you can with a raw array. Listing 11-9 illustrates this additional feature.

TEST_CASE("ScopedArray supports operator[]") {
  boost::scoped_array<int> squares{
    new int[5] { 0, 4, 9, 16, 25 }
  };
  squares[0] = 1; 
  REQUIRE(squares[0] == 1); 
  REQUIRE(squares[1] == 4);
  REQUIRE(squares[2] == 9);
}

Listing 11-9: The boost::scoped_array implements operator[].

You declare a scoped_array the same way you declare a scoped_ptr, by using a single template parameter . In the case of scoped_array, the template parameter is the type contained by the array , not the type of the array. You pass in a dynamic array to the constructor of squares, making the dynamic array squares the array’s owner. You can use operator[] to write and read elements.

A Partial List of Supported Operations

So far, you’ve learned about the major features of scoped pointers. For reference, Table 11-1 enumerates all the operators discussed, plus a few that haven’t been covered yet. In the table, ptr is a raw pointer and s_ptr is a scoped pointer. See the Boost documentation for more information.

Table 11-1: All of the Supported boost::scoped_ptr Operations

Operation

Notes

scoped_ptr<...>{ } or scoped_ptr <...>{ nullptr }

Creates an empty scoped pointer.

scoped_ptr <...>{ ptr }

Creates a scoped pointer owning the dynamic object pointed to by ptr.

~scoped_ptr<...>()

Calls delete on the owned object if full.

s_ptr1.swap(s_ptr2)

Exchanges owned objects between s_ptr1 and s_ptr2.

swap(s_ptr1, s_ptr2)

A free function identical to the swap method.

s_ptr.reset()

If full, calls delete on object owned by s_ptr.

s_ptr.reset(ptr)

Deletes currently owned object and then takes ownership of ptr.

ptr = s_ptr.get()

Returns the raw pointer ptr; s_ptr retains ownership.

*s_ptr

Dereferences operator on owned object.

s_ptr->

Member dereferences operator on owned object.

bool{ s_ptr }

bool conversion: true if full, false if empty.

Unique Pointers

A unique pointer has transferable, exclusive ownership over a single dynamic object. You can move unique pointers, which makes them transferable. They also have exclusive ownership, so they cannot be copied. The stdlib has a unique_ptr available in the <memory> header.

NOTE

Boost doesn’t offer a unique pointer.

Constructing

The std::unique_ptr takes a single template parameter corresponding to the pointed-to type, as in std::unique_ptr<int> for a “unique pointer to int” type.

As with a scoped pointer, the unique pointer has a default constructor that initializes the unique pointer to empty. It also provides a constructor taking a raw pointer that takes ownership of the pointed-to dynamic object. One construction method is to create a dynamic object with new and pass the result to the constructor, like this:

std::unique_ptr<int> my_ptr{ new int{ 808 } };

Another method is to use the std::make_unique function. The make_unique function is a template that takes all the arguments and forwards them to the appropriate constructor of the template parameter. This obviates the need for new. Using std::make_unique, you could rewrite the preceding object initialization as:

auto my_ptr = make_unique<int>(808);

The make_unique function was created to avoid some devilishly subtle memory leaks that used to occur when you used new with previous versions of C++. However, in the latest version of C++, these memory leaks no longer occur. Which constructor you use mainly depends on your preference.

Supported Operations

The std::unique_ptr function supports every operation that boost::scoped_ptr supports. For example, you can use the following type alias as a drop-in replacement for ScopedOathbreakers in Listings 11-1 to 11-7:

using UniqueOathbreakers = std::unique_ptr<DeadMenOfDunharrow>;

One of the major differences between unique and scoped pointers is that you can move unique pointers because they’re transferable.

Transferable, Exclusive Ownership

Not only are unique pointers transferable, but they have exclusive ownership (you cannot copy them). Listing 11-10 illustrates how you can use the move semantics of unique_ptr.

TEST_CASE("UniquePtr can be used in move") {
  auto aragorn = std::make_unique<DeadMenOfDunharrow>(); 
  SECTION("construction") {
    auto son_of_arathorn{ std::move(aragorn) }; 
    REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 1); 
  }
  SECTION("assignment") {
    auto son_of_arathorn = std::make_unique<DeadMenOfDunharrow>(); 
    REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 2); 
    son_of_arathorn = std::move(aragorn); 
    REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 1); 
  }
}

Listing 11-10: The std::unique_ptr supports move semantics for transferring ownership.

This listing creates a unique_ptr called aragorn that you use in two separate tests.

In the first test, you move aragorn with std::move into the move constructor of son_of_arathorn . Because aragorn transfers ownership of its DeadMenOfDunharrow to son_of_arathorn, the oaths_to_fulfill object still only has value 1 .

The second test constructs son_of_arathorn via make_unique , which pushes the oaths_to_fulfill to 2 . Next, you use the move assignment operator to move aragorn into son_of_arathorn . Again, aragorn transfers ownership to son_of_aragorn. Because son_of_aragorn can own only one dynamic object at a time, the move assignment operator destroys the currently owned object before emptying the dynamic object of aragorn. This results in oaths_to_fulfill decrementing to 1 .

Unique Arrays

Unlike boost::scoped_ptr, std::unique_ptr has built-in dynamic array support. You just use the array type as the template parameter in the unique pointer’s type, as in std::unique_ptr<int[]>.

It’s very important that you don’t initialize a std::unique_ptr<T> with a dynamic array T[]. Doing so will cause undefined behavior, because you’ll be causing a delete of an array (rather than delete[]). The compiler cannot save you, because operator new[] returns a pointer that is indistinguishable from the kind returned by operator new.

Like scoped_array, a unique_ptr to array type offers operator[] for accessing elements. Listing 11-11 demonstrates this concept.

TEST_CASE("UniquePtr to array supports operator[]") {
  std::unique_ptr<int[]> squares{
    new int[5]{ 1, 4, 9, 16, 25 } 
  };
  squares[0] = 1; 
  REQUIRE(squares[0] == 1); 
  REQUIRE(squares[1] == 4);
  REQUIRE(squares[2] == 9);
}

Listing 11-11: The std::unique_ptr to an array type supports operator[].

The template parameter int[] indicates to std::unique_ptr that it owns a dynamic array. You pass in a newly minted dynamic array and then use operator[] to set the first element ; then you use operator[] to retrieve elements .

Deleters

The std::unique_ptr has a second, optional template parameter called its deleter type. A unique pointer’s deleter is what gets called when the unique pointer needs to destroy its owned object.

A unique_ptr instantiation contains the following template parameters:

std::unique_ptr<T, Deleter=std::default_delete<T>>

The two template parameters are T, the type of the owned dynamic object, and Deleter, the type of the object responsible for freeing an owned object. By default, Deleter is std::default_delete<T>, which calls delete or delete[] on the dynamic object.

To write a custom deleter, all you need is a function-like object that is invokable with a T*. (The unique pointer will ignore the deleter’s return value.) You pass this deleter as the second parameter to the unique pointer’s constructor, as shown in Listing 11-12.

#include <cstdio>

auto my_deleter = [](int* x) { 
  printf("Deleting an int at %p.", x);
  delete x;
};
std::unique_ptr<int, decltype(my_deleter)> my_up{
  new int,
  my_deleter
};

Listing 11-12: Passing a custom deleter to a unique pointer

The owned object type is int , so you declare a my_deleter function object that takes an int* . You use decltype to set the deleter template parameter .

Custom Deleters and System Programming

You use a custom deleter whenever delete doesn’t provide the resource-releasing behavior you require. In some settings, you’ll never need a custom deleter. In others, like system programming, you might find them quite useful. Consider a simple example where you manage a file using the low-level APIs fopen, fprintf, and fclose in the <cstdio> header.

The fopen function opens a file and has the following signature:

FILE* fopen(const char *filename, const char *mode);

On success, fopen returns a non-nullptr-valued FILE* . On failure, fopen returns nullptr and it sets the static int variable errno equal to an error code, like access denied (EACCES = 13) or no such file (ENOENT = 2).

NOTE

See the errno.h header for a listing of all error conditions and their corresponding int values.

The FILE* file handle is a reference to a file the operating system manages. A handle is an opaque, abstract reference to some resource in an operating system. The fopen function takes two arguments: filename is the path to the file you want to open, and mode is one of the six options shown in Table 11-2.

Table 11-2: All Six mode Options for fopen

String

Operations

File exists:

File doesn’t exist:

Notes

r

Read

fopen fails

w

Write

Overwrite

Create it

If the file exists, all contents are discarded.

a

Append

Create it

Always write to the end of the file.

r+

Read/Write

fopen fails

w+

Read/Write

Overwrite

Create it

If the file exists, all contents are discarded.

a+

Read/Write

Create it

Always write to the end of the file.

You must close the file manually with fclose once you’re done using it. Failure to close file handles is a common source of resource leakages, like so:

void fclose(FILE* file);

To write to a file, you can use the fprintf function, which is like a printf that prints to a file instead of the console. The fprintf function has identical usage to printf except you provide a file handle as the first argument before the format string:

int fprintf(FILE* file, const char* format_string, ...);

On success, fprintf returns the number of characters written to the open file . The format_string is the same as the format string for printf , as are the variadic arguments .

You can use a std::unique_ptr to a FILE. Obviously, you don’t want to call delete on the FILE* file handle when you’re ready to close the file. Instead, you need to close with fclose. Because fclose is a function-like object accepting a FILE*, it’s a suitable deleter.

The program in Listing 11-13 writes the string HELLO, DAVE. to the file HAL9000 and uses a unique pointer to perform resource management over the open file.

#include <cstdio>
#include <memory>

using FileGuard = std::unique_ptr<FILE, int(*)(FILE*)>; 

void say_hello(FileGuard file) {
  fprintf(file.get(), "HELLO DAVE"); 
}

int main() {
  auto file = fopen("HAL9000", "w"); 
  if (!file) return errno; 
  FileGuard file_guard{ file, fclose }; 
  // File open here
  say_hello(std::move(file_guard)); 
  // File closed here
  return 0;
}

Listing 11-13: A program using a std::unique_ptr and a custom deleter to manage a file handle

This listing makes the FileGuard type alias for brevity. (Notice the deleter type matches the type of fclose.) Next is a say_hello function that takes a FileGuard by value . Within say_hello, you fprintf HELLO DAVE to the file . Because the lifetime of file is bound to say_hello, the file gets closed once say_hello returns. Within main, you open the file HAL9000 in w mode, which will create or overwrite the file, and you save the raw FILE* file handle into file . You check whether file is nullptr, indicating an error occurred, and return with errno if HAL9000 couldn’t be opened . Next, you construct a FileGuard by passing the file handle file and the custom deleter fclose . At this point, the file is open, and thanks to its custom deleter, file_guard manages the file’s lifetime automatically.

To call say_hello, you need to transfer ownership into that function (because it takes a FileGuard by value) . Recall from “Value Categories” on page 124 that variables like file_guard are lvalues. This means you must move it into say_hello with std::move, which writes HELLO DAVE to the file. If you omit std::move, the compiler would attempt to copy it into say_hello. Because unique_ptr has a deleted copy constructor, this would generate a compiler error.

When say_hello returns, its FileGuard argument destructs and the custom deleter calls fclose on the file handle. Basically, it’s impossible to leak the file handle. You’ve tied it to the lifetime of FileGuard.

A Partial List of Supported Operations

Table 11-3 enumerates all the supported std::unique_ptr operations. In this table, ptr is a raw pointer, u_ptr is a unique pointer, and del is a deleter.

Table 11-3: All of the Supported std::unique_ptr Operations

Operation

Notes

unique_ptr<...>{ } or unique_ptr<...>{ nullptr }

Creates an empty unique pointer with a std::default_delete<...> deleter.

unique_ptr<...>{ ptr }

Creates a unique pointer owning the dynamic object pointed to by ptr. Uses a std::default_delete<...> deleter.

unique_ptr<...>{ ptr, del }

Creates a unique pointer owning the dynamic object pointed to by ptr. Uses del as deleter.

unique_ptr<...>{ move(u_ptr) }

Creates a unique pointer owning the dynamic object pointed to by the unique pointer u_ptr. Transfers ownership from u_ptr to the newly created unique pointer. Also moves the deleter of u_ptr.

~unique_ptr<...>()

Calls deleter on the owned object if full.

u_ptr1 = move(u_ptr2)

Transfers ownership of owned object and deleter from u_ptr2 to u_ptr1. Destroys currently owned object if full.

u_ptr1.swap(u_ptr2)

Exchanges owned objects and deleters between u_ptr1 and u_ptr2.

swap(u_ptr1, u_ptr2)

A free function identical to the swap method.

u_ptr.reset()

If full, calls deleter on object owned by u_ptr.

u_ptr.reset(ptr)

Deletes currently owned object; then takes ownership of ptr.

ptr = u_ptr.release()

Returns the raw pointer ptr; u_ptr becomes empty. Deleter is not called.

ptr = u_ptr.get()

Returns the raw pointer ptr; u_ptr retains ownership.

*u_ptr

Dereference operator on owned object.

u_ptr->

Member dereference operator on owned object.

u_ptr[index]

References the element at index (arrays only).

bool{ u_ptr }

bool conversion: true if full, false if empty.

u_ptr1 == u_ptr2

u_ptr1 != u_ptr2

u_ptr1 > u_ptr2

u_ptr1 >= u_ptr2

u_ptr1 < u_ptr2

u_ptr1 <= u_ptr2

Comparison operators; equivalent to evaluating comparison operators on raw pointers.

u_ptr.get_deleter()

Returns a reference to the deleter.

Shared Pointers

A shared pointer has transferable, non-exclusive ownership over a single dynamic object. You can move shared pointers, which makes them transferable, and you can copy them, which makes their ownership non-exclusive.

Non-exclusive ownership means that a shared_ptr checks whether any other shared_ptr objects also own the object before destroying it. This way, the last owner is the one to release the owned object.

The stdlib has a std::shared_ptr available in the <memory> header, and Boost has a boost::shared_ptr available in the <boost/smart_ptr/shared_ptr.hpp> header. You’ll use the stdlib version here.

NOTE

Both the stdlib and Boost shared_ptr are essentially identical, with the notable exception that Boost’s shared pointer doesn’t support arrays and requires you to use the boost::shared_array class in <boost/smart_ptr/shared_array.hpp>. Boost offers a shared pointer for legacy reasons, but you should use the stdlib shared pointer.

Constructing

The std::shared_ptr pointer supports all the same constructors as std::unique_ptr. The default constructor yields an empty shared pointer. To instead establish ownership over a dynamic object, you can pass a pointer to the shared_ptr constructor, like so:

std::shared_ptr<int> my_ptr{ new int{ 808 } };

You also have a corollary std::make_shared template function that forwards arguments to the pointed-to type’s constructor:

auto my_ptr = std::make_shared<int>(808);

You should generally use make_shared. Shared pointers require a control block, which keeps track of several quantities, including the number of shared owners. When you use make_shared, you can allocate the control block and the owned dynamic object simultaneously. If you first use operator new and then allocate a shared pointer, you’re making two allocations instead of one.

NOTE

Sometimes you might want to avoid using make_shared. For example, if you’ll be using a weak_ptr, you’ll still need the control block even if you can deallocate the object. In such a situation, you might prefer to have two allocations.

Because a control block is a dynamic object, shared_ptr objects sometimes need to allocate dynamic objects. If you wanted to take control over how shared_ptr allocates, you could override operator new. But this is shooting a sparrow with a cannon. A more tailored approach is to provide an optional template parameter called an allocator type.

Specifying an Allocator

The allocator is responsible for allocating, creating, destroying, and deallocating objects. The default allocator, std::allocator, is a template class defined in the <memory> header. The default allocator allocates memory from dynamic storage and takes a template parameter. (You’ll learn about customizing this behavior with a user-defined allocator in “Allocators” on page 365).

Both the shared_ptr constructor and make_shared have an allocator type template parameter, making three total template parameters: the pointed-to type, the deleter type, and the allocator type. For complicated reasons, you only ever need to declare the pointed-to type parameter. You can think of the other parameter types as being deduced from the pointed-to type.

For example, here’s a fully adorned make_shared invocation including a constructor argument, a custom deleter, and an explicit std::allocator:

std::shared_ptr<int> sh_ptr{
  new int{ 10 },
  [](int* x) { delete x; } ,
  std::allocator<int>{} 
};

Here, you specify a single template parameter, int, for the pointed-to type . In the first argument, you allocate and initialize an int . Next is a custom deleter , and as a third argument you pass a std::allocator .

For technical reasons, you can’t use a custom deleter or custom allocator with make_shared. If you want a custom allocator, you can use the sister function of make_shared, which is std::allocate_shared. The std::allocate _shared function takes an allocator as the first argument and forwards the remainder of the arguments to the owned object’s constructor:

auto sh_ptr = std::allocate_shared<int>(std::allocator<int>{}, 10);

As with make_shared, you specify the owned type as a template parameter , but you pass an allocator as the first argument . The rest of the arguments forward to the constructor of int .

NOTE

For the curious, here are two reasons why you can’t use a custom deleter with make_shared. First, make_shared uses new to allocate space for the owned object and the control block. The appropriate deleter for new is delete, so generally a custom deleter wouldn’t be appropriate. Second, the custom deleter can’t generally know how to deal with the control block, only with the owned object.

It isn’t possible to specify a custom deleter with either make_shared or allocate_shared. If you want to use a custom deleter with shared pointers, you must use one of the appropriate shared_ptr constructors directly.

Supported Operations

The std::shared_ptr supports every operation that std::unique_ptr and boost::scoped_ptr support. You could use the following type alias as a drop-in replacement for ScopedOathbreakers in Listings 11-1 to 11-7 and UniqueOathbreakers from Listings 11-10 to 11-13:

using SharedOathbreakers = std::shared_ptr<DeadMenOfDunharrow>;

The major functional difference between a shared pointer and a unique pointer is that you can copy shared pointers.

Transferable, Non-Exclusive Ownership

Shared pointers are transferable (you can move them), and they have non-exclusive ownership (you can copy them). Listing 11-10, which illustrates a unique pointer’s move semantics, works the same for a shared pointer. Listing 11-14 demonstrates that shared pointers also support copy semantics.

TEST_CASE("SharedPtr can be used in copy") {
  auto aragorn = std::make_shared<DeadMenOfDunharrow>();
  SECTION("construction") {
    auto son_of_arathorn{ aragorn }; 
    REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 1); 
  }
  SECTION("assignment") {
    SharedOathbreakers son_of_arathorn; 
    son_of_arathorn = aragorn; 
    REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 1); 
  }
  SECTION("assignment, and original gets discarded") {
    auto son_of_arathorn = std::make_shared<DeadMenOfDunharrow>(); 
    REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 2);
    son_of_arathorn = aragorn; 
    REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 1); 
  }
}

Listing 11-14: The std::shared_ptr supports copy.

After constructing the shared pointer aragorn, you have three tests. The first test illustrates that the copy constructor that you use to build son_of_arathorn shares ownership over the same DeadMenOfDunharrow .

In the second test, you construct an empty shared pointer son_of _arathorn and then show that copy assignment also doesn’t change the number of DeadMenOfDunharrow .

The third test illustrates that when you construct the full shared pointer son_of_arathorn , the number of DeadMenOfDunharrow increases to 2 . When you copy assign aragorn to son_of_arathorn , the son_of_arathorn deletes its DeadMenOfDunharrow because it has sole ownership. It then increments the reference count of the DeadMenOfDunharrow owned by aragorn. Because both shared pointers own the same DeadMenOfDunharrow, the oaths_to_fulfill decrements from 2 to 1 .

Shared Arrays

A shared array is a shared pointer that owns a dynamic array and supports operator[]. It works just like a unique array except it has non-exclusive ownership.

Deleters

Deleters work the same way for shared pointers as they do for unique pointers except you don’t need to provide a template parameter with the deleter’s type. Simply pass the deleter as the second constructor argument. For example, to convert Listing 11-12 to use a shared pointer, you simply drop in the following type alias:

using FileGuard = std::shared_ptr<FILE>;

Now, you’re managing FILE* file handles with shared ownership.

A Partial List of Supported Operations

Table 11-4 provides a mostly complete listing of the supported constructors of shared_ptr. In this table, ptr is a raw pointer, sh_ptr is a shared pointer, u_ptr is a unique pointer, del is a deleter, and alc is an allocator.

Table 11-4: All of the Supported std::shared_ptr Constructors

Operation

Notes

shared_ptr<...>{ } or shared_ptr<...>{ nullptr }

Creates an empty shared pointer with a std::default_delete<T> and a std::allocator<T>.

shared_ptr<...>{ ptr, [del], [alc] }

Creates a shared pointer owning the dynamic object pointed to by ptr. Uses a std::default_delete<T> and a std::allocator<T> by default; otherwise, del as deleter, alc as allocator if supplied.

shared_ptr<...>{ sh_ptr }

Creates a shared pointer owning the dynamic object pointed to by the shared pointer sh_ptr. Copies ownership from sh_ptr to the newly created shared pointer. Also copies the deleter and allocator of sh_ptr.

shared_ptr<...>{ sh_ptr , ptr }

An aliasing constructor: the resulting shared pointer holds an unmanaged reference to ptr but participates in sh_ptr reference counting.

shared_ptr<...>{ move(sh_ptr) }

Creates a shared pointer owning the dynamic object pointed to by the shared pointer sh_ptr. Transfers ownership from sh_ptr to the newly created shared pointer. Also moves the deleter of sh_ptr.

shared_ptr<...>{ move(u_ptr) }

Creates a shared pointer owning the dynamic object pointed to by the unique pointer u_ptr. Transfers ownership from u_ptr to the newly created shared pointer. Also moves the deleter of u_ptr.

Table 11-5 provides a listing of most of the supported operations of std::shared_ptr. In this table, ptr is a raw pointer, sh_ptr is a shared pointer, u_ptr is a unique pointer, del is a deleter, and alc is an allocator.

Table 11-5: Most of the Supported std::shared_ptr Operations

Operation

Notes

~shared_ptr<...>()

Calls deleter on the owned object if no other owners exist.

sh_ptr1 = sh_ptr2

Copies ownership of owned object and deleter from sh_ptr2 to sh_ptr1. Increments number of owners by 1. Destroys currently owned object if no other owners exist.

sh_ptr = move(u_ptr)

Transfers ownership of owned object and deleter from u_ptr to sh_ptr. Destroys currently owned object if no other owners exist.

sh_ptr1 = move(sh_ptr2)

Transfers ownership of owned object and deleter from sh_ptr2 to sh_ptr1. Destroys currently owned object if no other owners exist.

sh_ptr1.swap(sh_ptr2)

Exchanges owned objects and deleters between sh_ptr1 and sh_ptr2.

swap(sh_ptr1, sh_ptr2)

A free function identical to the swap method.

sh_ptr.reset()

If full, calls deleter on object owned by sh_ptr if no other owners exist.

sh_ptr.reset(ptr, [del], [alc])

Deletes currently owned object if no other owners exist; then takes ownership of ptr. Can optionally provide deleter del and allocator alc. These default to std::default_delete<T> and std::allocator<T>.

ptr = sh_ptr.get()

Returns the raw pointer ptr; sh_ptr retains ownership.

*sh_ptr

Dereference operator on owned object.

sh_ptr->

Member dereference operator on owned object.

sh_ptr.use_count()

References the total number of shared pointers owning the owned object; zero if empty.

sh_ptr[index]

Returns the element at index (arrays only).

bool{ sh_ptr }

bool conversion: true if full, false if empty.

sh_ptr1 == sh_ptr2

sh_ptr1 != sh_ptr2

sh_ptr1 > sh_ptr2

sh_ptr1 >= sh_ptr2

sh_ptr1 < sh_ptr2

sh_ptr1 <= sh_ptr2

Comparison operators; equivalent to evaluating comparison operators on raw pointers.

sh_ptr.get_deleter()

Returns a reference to the deleter.

Weak Pointers

A weak pointer is a special kind of smart pointer that has no ownership over the object to which it refers. Weak pointers allow you to track an object and to convert the weak pointer into a shared pointer only if the tracked object still exists. This allows you to generate temporary ownership over an object. Like shared pointers, weak pointers are movable and copyable.

A common usage for weak pointers is caches. In software engineering, a cache is a data structure that stores data temporarily so it can be retrieved faster. A cache could keep weak pointers to objects so they destruct once all other owners release them. Periodically, the cache can scan its stored weak pointers and trim those with no other owners.

The stdlib has a std::weak_ptr, and Boost has a boost::weak_ptr. These are essentially identical and are only meant to be used with their respective shared pointers, std::shared_ptr and boost::shared_ptr.

Constructing

Weak pointer constructors are completely different from scoped, unique, and shared pointers because weak pointers don’t directly own dynamic objects. The default constructor constructs an empty weak pointer. To construct a weak pointer that tracks a dynamic object, you must construct it using either a shared pointer or another weak pointer.

For example, the following passes a shared pointer into the weak pointer’s constructor:

auto sp = std::make_shared<int>(808);
std::weak_ptr<int> wp{ sp };

Now the weak pointer wp will track the object owned by the shared pointer sp.

Obtaining Temporary Ownership

Weak pointers invoke their lock method to get temporary ownership of their tracked object. The lock method always creates a shared pointer. If the tracked object is alive, the returned shared pointer owns the tracked object. If the tracked object is no longer alive, the returned shared pointer is empty. Consider the example in Listing 11-15.

TEST_CASE("WeakPtr lock() yields") {
  auto message = "The way is shut.";
  SECTION("a shared pointer when tracked object is alive") {
    auto aragorn = std::make_shared<DeadMenOfDunharrow>(message); 
    std::weak_ptr<DeadMenOfDunharrow> legolas{ aragorn }; 
    auto sh_ptr = legolas.lock(); 
    REQUIRE(sh_ptr->message == message); 
    REQUIRE(sh_ptr.use_count() == 2); 
  }
  SECTION("empty when shared pointer empty") {
    std::weak_ptr<DeadMenOfDunharrow> legolas;
    {
      auto aragorn = std::make_shared<DeadMenOfDunharrow>(message); 
      legolas = aragorn; 
    }
    auto sh_ptr = legolas.lock(); 
    REQUIRE(nullptr == sh_ptr); 
  }
}

Listing 11-15: The std::weak_ptr exposes a lock method for obtaining temporary ownership.

In the first test, you create the shared pointer aragorn with a message. Next, you construct a weak pointer legolas using aragorn . This sets up legolas to track the dynamic object owned by aragorn. When you call lock on the weak pointer , aragorn is still alive, so you obtain the shared pointer sh_ptr, which also owns the same DeadMenOfDunharrow. You confirm this by asserting that the message is the same and that the use count is 2 .

In the second test, you also create an aragorn shared pointer , but this time you use the assignment operator , so the previously empty weak pointer legolas now tracks the dynamic object owned by aragorn. Next, aragorn falls out of block scope and dies. This leaves legolas tracking a dead object. When you call lock at this point , you obtain an empty shared pointer .

Advanced Patterns

In some advanced usages of shared pointers, you might want to create a class that allows instances to create shared pointers referring to themselves. The std::enable_shared_from_this class template implements this behavior. All that’s required from a user perspective is to inherit from enable_shared _from_this in the class definition. This exposes the shared_from_this and weak_from_this methods, which produce either a shared_ptr or a weak_ptr referring to the current object. This is a niche case, but if you want to see more details, refer to [util.smartptr.enab].

Supported Operations

Table 11-6 lists most of the supported weak pointer operations. In this table, w_ptr is a weak pointer, and sh_ptr is a shared pointer.

Table 11-6: Most of the Supported std::shared_ptr Operations

Operation

Notes

weak_ptr<...>{ }

Creates an empty weak pointer.

weak_ptr<...>{ w_ptr } or weak_ptr<...>{ sh_ptr }

Tracks the object referred to by weak pointer w_ptr or shared pointer sh_ptr.

weak_ptr<...>{ move(w_ptr) }

Tracks the object referred to by w_ptr; then empties w_ptr.

~weak_ptr<...>()

Has no effect on the tracked object.

w_ptr1 = sh_ptr or w_ptr1 = w_ptr2

Replaces currently tracked object with the object owned by sh_ptr or tracked by w_ptr2.

w_ptr1 = move(w_ptr2)

Replaces currently tracked object with object tracked by w_ptr2. Empties w_ptr2.

sh_ptr = w_ptr.lock()

Creates the shared pointer sh_ptr owning the object tracked by w_ptr. If the tracked object has expired, sh_ptr is empty.

w_ptr1.swap(w_ptr2)

Exchanges tracked objects between w_ptr1 and w_ptr2.

swap(w_ptr1, w_ptr2)

A free function identical to the swap method.

w_ptr.reset()

Empties the weak pointer.

w_ptr.use_count()

Returns the number of shared pointers owning the tracked object.

w_ptr.expired()

Returns true if the tracked object has expired, false if it hasn’t.

sh_ptr.use_count()

Returns the total number of shared pointers owning the owned object; zero if empty.

Intrusive Pointers

An intrusive pointer is a shared pointer to an object with an embedded reference count. Because shared pointers usually keep reference counts, they’re not suitable for owning such objects. Boost provides an implementation called boost::intrusive_ptr in the <boost/smart_ptr/intrusive_ptr.hpp> header.

It’s rare that a situation calls for an intrusive pointer. But sometimes you’ll use an operating system or a framework that contains embedded references. For example, in Windows COM programming an intrusive pointer can be very useful: COM objects that inherit from the IUnknown interface have an AddRef and a Release method, which increment and decrement an embedded reference count (respectively).

Each time an intrusive_ptr is created, it calls the function intrusive_ptr_add_ref. When an intrusive_ptr is destroyed, it calls the intrusive_ptr_release free function. You’re responsible for freeing appropriate resources in intrusive_ptr_release when the reference count falls to zero. To use intrusive_ptr, you must provide a suitable implementation of these functions.

Listing 11-16 demonstrates intrusive pointers using the DeadMenOfDunharrow class. Consider the implementations of intrusive_ptr_add_ref and intrusive_ptr_release in this listing.

#include <boost/smart_ptr/intrusive_ptr.hpp>

using IntrusivePtr = boost::intrusive_ptr<DeadMenOfDunharrow>; 
size_t ref_count{}; 

void intrusive_ptr_add_ref(DeadMenOfDunharrow* d) {
  ref_count++; 
}

void intrusive_ptr_release(DeadMenOfDunharrow* d) {
  ref_count--; 
  if (ref_count == 0) delete d; 
}

Listing 11-16: Implementations of intrusive_ptr_add_ref and intrusive_ptr_release

Using the type alias IntrusivePtr saves some typing . Next, you declare a ref_count with static storage duration . This variable keeps track of the number of living intrusive pointers. In intrusive_ptr_add_ref, you increment ref_count . In intrusive_ptr_release, you decrement ref_count . When ref _count drops to zero, you delete the DeadMenOfDunharrow argument .

NOTE

It’s absolutely critical that you use only a single DeadMenOfDunharrow dynamic object with intrusive pointers when using the setup in Listing 11-16. The ref_count approach will correctly track only a single object. If you have multiple dynamic objects owned by different intrusive pointers, the ref_count will become invalid, and you’ll get incorrect delete behavior .

Listing 11-17 shows how to use the setup in Listing 11-16 with intrusive pointers.

TEST_CASE("IntrusivePtr uses an embedded reference counter.") {
  REQUIRE(ref_count == 0); 
  IntrusivePtr aragorn{ new DeadMenOfDunharrow{} }; 
  REQUIRE(ref_count == 1); 
  {
    IntrusivePtr legolas{ aragorn }; 
    REQUIRE(ref_count == 2); 
  }
  REQUIRE(DeadMenOfDunharrow::oaths_to_fulfill == 1); 
}

Listing 11-17: Using a boost::intrusive_ptr

This test begins by checking that ref_count is zero . Next, you construct an intrusive pointer by passing a dynamically allocated DeadMenOfDunharrow . This increases ref_count to 1, because creating an intrusive pointer invokes intrusive_ptr_add_ref . Within a block scope, you construct another intrusive pointer legolas that shares ownership with aragorn . This increases the ref_count to 2 , because creating an intrusive pointer invokes intrusive_ptr_add_ref. When legolas falls out of block scope, it destructs, causing intrusive_ptr_release to invoke. This decrements ref_count to 1 but doesn’t cause the owned object to delete .

Summary of Smart Pointer Options

Table 11-7 summarizes all the smart pointer options available to use in stdlib and Boost.

Table 11-7: Smart Pointers in stdlib and Boost

Type name

stdlib header

Boost header

Movable/transferable ownership

Copyable/non-exclusive ownership

scoped_ptr

<boost/smart_ptr/scoped_ptr.hpp>

scoped_array

<boost/smart_ptr/scoped_array.hpp>

unique_ptr

<memory>

shared_ptr

<memory>

<boost/smart_ptr/shared_ptr.hpp>

shared_array

<boost/smart_ptr/shared_array.hpp>

weak_ptr

<memory>

<boost/smart_ptr/weak_ptr.hpp>

intrusive_ptr

<boost/smart_ptr/intrusive_ptr.hpp>

Allocators

Allocators are low-level objects that service requests for memory. The stdlib and Boost libraries enable you to provide allocators to customize how a library allocates dynamic memory.

In the majority of cases, the default allocator std::allocate is totally sufficient. It allocates memory using operator new(size_t), which allocates raw memory from the free store, also known as the heap. It deallocates memory using operator delete(void*), which deallocates the raw memory from the free store. (Recall from “Overloading Operator new” on page 189 that operator new and operator delete are defined in the <new> header.)

In some settings, such as gaming, high-frequency trading, scientific analyses, and embedded applications, the memory and computational overhead associated with the default free store operations is unacceptable. In such settings, it’s relatively easy to implement your own allocator. Note that you really shouldn’t implement a custom allocator unless you’ve conducted some performance testing that indicates that the default allocator is a bottleneck. The idea behind a custom allocator is that you know a lot more about your specific program than the designers of the default allocator model, so you can make improvements that will increase allocation performance.

At a minimum, you need to provide a template class with the following characteristics for it to work as an allocator:

  • An appropriate default constructor
  • A value_type member corresponding to the template parameter
  • A template constructor that can copy an allocator’s internal state while dealing with a change in value_type
  • An allocate method
  • A deallocate method
  • An operator== and an operator!=

The MyAllocator class in Listing 11-18 implements a simple, pedagogical variant of std::allocate that keeps track of how many allocations and deallocations you’ve made.

#include <new>

static size_t n_allocated, n_deallocated;

template <typename T>
struct MyAllocator {
  using value_type = T; 
  MyAllocator() noexcept{ } 
  template <typename U>
  MyAllocator(const MyAllocator<U>&) noexcept { } 
  T* allocate(size_t n) { 
    auto p = operator new(sizeof(T) * n);
    ++n_allocated;
    return static_cast<T*>(p);
  }
  void deallocate(T* p, size_t n) { 
    operator delete(p);
    ++n_deallocated;
  }
};

template <typename T1, typename T2>
bool operator==(const MyAllocator<T1>&, const MyAllocator<T2>&) {
  return true; 
}
template <typename T1, typename T2>
bool operator!=(const MyAllocator<T1>&, const MyAllocator<T2>&) {
  return false; 
}

Listing 11-18: A MyAllocator class modeled after std::allocate

First, you declare the value_type type alias for T, one of the requirements for implementing an allocator . Next is a default constructor and a template constructor . Both of these are empty because the allocator doesn’t have state to pass on.

The allocate method models std::allocate by allocating the requisite number of bytes, sizeof(T) * n, using operator new. Next, it increments the static variable n_allocated so you can keep track of the number of allocations for testing purposes. The allocate method then returns a pointer to the newly allocated memory after casting void* to the relevant pointer type.

The deallocate method also models std::allocate by calling operator delete. As an analogy to allocate, it increments the n_deallocated static variable for testing and returns.

The final task is to implement an operator== and an operator!= taking the new class template. Because the allocator has no state, any instance is the same as any other instance, so operator== returns true and operator!= returns true .

NOTE

Listing 11-18 is a teaching tool and doesn’t actually make allocations any more efficient. It simply wraps the call to new and delete.

So far, the only class you know about that uses an allocator is std::shared _ptr. Consider how Listing 11-19 uses MyAllocator with std::allocate shared.

TEST_CASE("Allocator") {
  auto message = "The way is shut.";
  MyAllocator<DeadMenOfDunharrow> alloc; 
  {
    auto aragorn = std::allocate_shared<DeadMenOfDunharrow>(my_alloc, message);
    REQUIRE(aragorn->message == message); 
    REQUIRE(n_allocated == 1); 
    REQUIRE(n_deallocated == 0); 
  }
  REQUIRE(n_allocated == 1); 
  REQUIRE(n_deallocated == 1); 
}

Listing 11-19: Using MyAllocator with std::shared_ptr

You create a MyAllocator instance called alloc . Within a block, you pass alloc as the first argument to allocate_shared , which creates the shared pointer aragorn containing a custom message . Next, you confirm that aragorn contains the correct message , n_allocated is 1 , and n_deallocated is 0 .

After aragorn falls out of block scope and destructs, you verify that n_allocated is still 1 and n_deallocated is now 1 .

NOTE

Because allocators handle low-level details, you can really get down into the weeds when specifying their behavior. See [allocator.requirements] in the ISO C++ 17 Standard for a thorough treatment.

Summary

Smart pointers manage dynamic objects via RAII, and you can provide allocators to customize dynamic memory allocation. Depending on which smart pointer you choose, you can encode different ownership patterns onto the dynamic object.

EXERCISES

11-1. Reimplement Listing 11-12 to use a std::shared_ptr rather than a std::unique_ptr. Notice that although you’ve relaxed the ownership requirements from exclusive to non-exclusive, you’re still transferring ownership to the say_hello function.

11-2. Remove the std::move from the call to say_hello. Then make an additional call to say_hello. Notice that the ownership of file_guard is no longer transferred to say_hello. This permits multiple calls.

11-3. Implement a Hal class that accepts a std::shared_ptr<FILE> in its constructor. In Hal’s destructor, write the phrase Stop, Dave. to the file handle held by your shared pointer. Implement a write_status function that writes the phrase I'm completely operational. to the file handle. Here’s a class declaration you can work from:

struct Hal {
  Hal(std::shared_ptr<FILE> file);
  ~Hal();
  void write_status();
  std::shared_ptr<FILE> file;
};

11-4. Create several Hal instances and invoke write_status on them. Notice that you don’t need to keep track of how many Hal instances are open: file management gets handled via the shared pointer’s shared ownership model.

FURTHER READING

  • ISO International Standard ISO/IEC (2017) — Programming Language C++ (International Organization for Standardization; Geneva, Switzerland; https://isocpp.org/std/the-standard/)
  • The C++ Programming Language, 4th Edition, by Bjarne Stroustrup (Pearson Education, 2013)
  • The Boost C++ Libraries, 2nd Edition, by Boris Schäling (XML Press, 2014)
  • The C++ Standard Library: A Tutorial and Reference, 2nd Edition, by Nicolai M. Josuttis (Addison-Wesley Professional, 2012)
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset