Unique Pointers

Unique pointers, and really anything else that's "unique" in C++, model exclusive ownership. They offer practically no space or time overhead as compared to raw pointers. Because they model exclusive ownership, they cannot be copied, only moved. Here's a bad, quick, and dirty example of how something like this might be implemented.

template<typename T>
class BadUniquePtr {
    T * data;

    void freeData() {
        if (data) {
            delete data;
            data = nullptr;
        }
    }
public:
    BadUniquePtr(T * owningPtr) : data(owningPtr) {}
    BadUniquePtr() : data(nullptr) {}

    BadUniquePtr(const BadUniquePtr &) = delete;
    BadUniquePtr& operator=(const BadUniquePtr &) = delete;

    BadUniquePtr(BadUniquePtr && other) : BadUniquePtr() {
        swap(data, other.data);
    }

    BadUniquePtr& operator=(BadUniquePtr && other) {
       freeData();
       swap(data, other.data);
    }

    ~BadUniquePtr() {
        freeData();
    }
}

std::unique_ptr is a template, and so when you instantiate it with a type it replaces every template argument (T in BadUniquePtr) with whatever type you are instantiating it with.

std::unique_ptr provides the reset() member function to manually free the data or swap the internal pointer with a different one. If we need to, we can get access to the internal pointer with get() or release(). The latter returns the pointer and releases it from the unique_ptrs management. We can also swap the owning data between two unique_ptrs with the swap() member function. std::unique_ptr also provide operator* and operator-> so they can be de-referenced just like raw pointers.

auto unique = std::make_unique<double>(6.28);
double* ptr = unique.get(); // the unique ptr is a class itself
// to access the unique_ptrs members use the dot operator
// to access the underlying data's members use ->

//ptr is not an owning pointer

auto u2 = std::make_unique<double>(3.14);
unique.swap(u2);
// Smart pointers must be pointers to the same type to swap

double* owningPtr = u2.release();
// u2 no longer manages the data

unique.reset(); // free data and set to nullptr
// which is the same as
unique = nullptr;

auto u3 = std::make_unique<double>(2.67);

unique = u3; // error, cannot copy
unique = owningPtr; // takes ownership of pointer

unique.reset(new double(-1.12)); // free data and take ownership of new raw ptr


// ---

auto getPtr() {
    return std::make_ptr<long long>(19474579);
    // move (Pre C++17), good
}

auto u4 = getPtr(); // move ctor, good
auto u5 = std::move(u4); // move ctor, good

u4 = std::move(u5); // move assignment, good

Smart pointers can store arrays as well. They handle calling the correct delete too. A smart pointer wrapped around an array overloads operator[] to provide index access. While this is slightly better than unmanaged arrays, an std::vector would be better because the smart pointer still does not store the size of the array.

A pointer of an array is a pointer to the first element in the array since arrays are stored contiguously in memory. A C string is a char * (or const char *) that is an array of characters with the last one being the null terminator ('\0' which is 0).

std::unique_ptr<char[]> string = std::make_unique<char[]>(100);
// 100 length char array
string[0] = 'h';
string[1] = 'i';
string[2] = '\0';
*string; // 'h'
// pointer is to the first element of the array

const char * cStr = string.get();

std::cout << cStr; // "hi"

Deleters

This is all well and good if we want to use C++'s delete or delete[]. But what if we have a custom allocator or want to use C style allocation. Well luckily, smart pointers abstract away how they delete the data and delegate that responsibility to a deleter. The default deleter uses delete or delete[] depending on the type it wraps (delete for T, delete[] for T[]). The actual definition of std::unique_ptr is

template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr // ..

In reality there is a second template argument! This type should be the type of a callable object that overloads operator() to accept a pointer to the underlying data. operator() would then be responsible for performing the logic to cleanup the passed pointer. With a custom deleter, we cannot use std::make_unique because this function abstracts away the usage of the default deleter's matching allocation functions. Thus, we must use the unique_ptr constructor, passing a pointer as the first argument and an instance of the deleter type as the second.

struct Foo {
    int num;

    Foo() : num(0) {}
}

struct FooDeleter {
    void operator()(Foo * ptr) {
        ptr->~Foo();
        free(ptr);
    }
}

Foo * makeFoo() {
    const auto mem = malloc(sizeof(Foo));
    return 
        new (mem) Foo(); // placement new, calls constructor and constructs object
        // in already allocated memory
        // once again, more on this later

}

std::unique_ptr<Foo, FooDeleter> fooPtr(makeFoo(), FooDeleter());
// use fooPtr like normal

fooPtr->num = 100;

Now this seems like a bit of boilerplate just for two function calls. Well, we'll explain the following soon enough, but here's some other ways of doing the same thing:


void freeFoo(Foo * ptr) {
    ptr->~Foo();
    free(ptr);
}

std::unique_ptr<Foo, std::function<void(Foo*)>> fp2(makeFoo(), [](Foo * ptr) {
    ptr->~Foo();
    free(ptr);
});

// Use a lambda as the callable object
// passes a Foo* and returns void

std::unique_ptr<Foo, void(*)(Foo*)> fp3(makeFoo(), &freeFoo);
// function pointer is the deleter, pass free directly
// pointer to a function that takes a void* and returns void

std::unique_ptr<Foo, decltype(&freeFoo)> fp4(makeFoo(), &freeFoo); 
// same as fp3, just slightly easier since function pointer syntax is a pain

std::unique_ptr<Foo, std::function<void(Foo*)>> fp5(makeFoo(), &freeFoo);
// same as fp3 and fp4 but instead use a generalized function as the type

Here's another example of a custom deleter that adds some housekeeping to the standard C++ allocation.

// There's a few issues with this, but this is mainly to demonstrate deleters
/// Invariant: contains pointers to active allocations or nullptr to indicate freed memory
std::vector<Bar*> allocations;

auto makeBar() { // strong
    allocations.push_back(nullptr); //may reallocate and move entire vector (more on this later), strong
    auto ptr = new Bar();
    const auto idx = allocations.size() - 1; // no throw
    allocations[idx] = ptr; //copying pointer cannot throw
    const auto deleter = [idx, &allocations](Bar * del) {
        delete del;
        allocations[idx] = nullptr;
    }; // noexcept (construction of lambda and deleter itself)
    return std::unique_ptr<Bar, std::function<void(Bar*)>>(ptr, deleter); //noexcept
}

auto barPtr = makeBar();

By the way, deleters work the same for shared_ptr too