The original title of this article was “RAII all the things?”, and the article didn’t have enough focus. It’s since been reworked.

Intro

std::unique_ptr is the prime example of RAII (Resource Acquisition Is Initialization). It’s the standard way of managing dynamically allocated memory:

{
    // ptr is std::unique_ptr<int>
    auto ptr = std::make_unique<int>(42); // calls new
} // runs destructor, calls delete

In fact, unique_ptr can be customized to manage many more resources, not just memory. For example, file handles, connection, opaque pointer to C context, and so on.

The trick is to provide a custom deleter:

template<
    class T,
    class Deleter // Customization point
> class unique_ptr;

We will look at few examples.

Smart File Pointer

Let’s start with something simple:

struct fcloser {
    void operator()(std::FILE* fp) const noexcept {
        std::fclose(fp);
    }
};

using file_ptr = std::unique_ptr<std::FILE, fcloser>;

Note that:

  • The deleter is simply a callable, with pointer as the only argument
  • There’s no need to check for null-ness; unique_ptr does it for us
  • The call operator is noexcept, since good destructors don’t throw

Memory-mapped File

Sometimes, the clean-up function is a bit more complex.

In POSIX, we have mmap that maps the process’s virtual address space to some file (“allocation”), and the corresponding deallocation function munmap:

// https://man7.org/linux/man-pages/man2/mmap.2.html

// Create a new mapping
//   On success, returns a pointer to the mapped area
//   On failure, returns MAP_FAILED
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);

// Delete the mapping
int munmap(void* addr, size_t length);

Note that munmap takes a pointer and a length. We can accommodate this by having the deleter remember the length:

struct mem_unmapper {
    size_t length{};

    void operator()(void* addr) const noexcept {
        ::munmap(addr, length);
    }
};

using mapped_mem_ptr = std::unique_ptr<void, mem_unmapper>;

Note that we can have a unique_ptr to void! This is syntactically fine, so long as we don’t dereference (*) or member-access (->) it.

Semantically, we shouldn’t interpret unique_ptr<void, ...> as “managing voidness”, but rather:

  • We have a pointer, void*, that refers to some resource (in this case, a region of memory)
  • The pointer is used to clean up the resource referred to

As with all customized unique_ptr, it is recommended to provide a factory function to fill the role of make_unique:

[[nodiscard]] inline mapped_mem_ptr make_mapped_mem(void* addr, size_t length, int prot, int flags, int fd, off_t offset) {
    void* p = ::mmap(addr, length, prot, flags, fd, offset);
    if (p == MAP_FAILED) { // MAP_FAILED is not NULL
        return nullptr;
    }
    return {p, mem_unmapper{length}}; // unique_ptr owns a deleter, which remembers the length
}

File Descriptor

For some resource, there isn’t even a pointer to begin with. An example would be the UNIX file fescriptor, which is just an int. Can we still use unique_ptr for it?

The answer is yes. It turns out that unique_ptr<T, D>::pointer doesn’t need to be an actual pointer; it can be any type that satisfies NullablePointer!

So can it be int?

… Not quite. A requirement of NullablePointer is that it must be able to construct from nullptr, which int cannot:

int i = nullptr;  // ill-formed, for good reasons

And even if it could, say initialized to 0 as int i = NULL; does, we probably don’t want this behavior: 0 is a valid file descriptor, and UNIX uses -1 as the sentinel value for invalidness/nullness.

Therefore, we must first roll up a wrapper type:

// Minimally satisfy NullablePointer. Intentionally non-RAII
class file_descriptor {
    int fd_{-1};
public:
    file_descriptor(int fd = -1): fd_(fd) {}
    file_descriptor(nullptr_t) {}
    operator int() const { return fd_; }
    explicit operator bool() const { return fd_ != -1; }
    friend bool operator==(file_descriptor, file_descriptor) = default; // Since C++20
};
// All functions can be marked noexcept, constexpr

Then the deleter:

struct fd_closer {
    using pointer = file_descriptor; // IMPORTANT
    void operator()(pointer fd) const noexcept {
        ::close(int(fd));
    }
};

The member typedef pointer is important. When it is present, unique_ptr will use it as the pointer type; otherwise, it falls back to T*.

So what we should use for T?

In this case, since we don’t do * or -> (the only places where T is used), T can literally be anything:

using unique_fd = std::unique_ptr<int,             fd_closer>; // Ok
using unique_fd = std::unique_ptr<file_descriptor, fd_closer>; // Ok
using unique_fd = std::unique_ptr<void,            fd_closer>; // Ok

… Even though they are all not quite right. We are not managing int or file_descriptor or void, but rather some UNIX file hidden from us.

For readablity, it’s worth making a opaque type:

struct unix_file;
using unique_fd = std::unique_ptr<unix_file,       fd_closer>; // Ok, recommended

The takeaway is: we can think of Deleter::pointer as a handle that refers to some resource.

On a side note, there’s a gotcha our unique_fd:

unique_fd fd{STDIN_FILENO};
assert(int(fd.get()) == STDIN_FILENO); // Fail!

This is because STDIN_FILENO is a macro that expands to 0, and a literal zero is also nullptr. Therefore, between the two viable constructor overloads:

unique_ptr( std::nullptr_t ); // (1)
unique_ptr( pointer );        // (2)

(1) is picked because it’s a direct match. Our file_descriptor, when constructed from nullptr, takes the value -1.

The fix is:

unique_fd fd{int(STDIN_FILENO)}; // Ok
unique_fd fd{file_descriptor(STDIN_FILENO)}; // Ok too

This is one of those “C++ being C++” moments and why I have a love/hate abusive relationship with it. Anyway, as suggested above, providing a factory function for your RAII class minimizes pitfalls like this.

unique_resource

If you feel like the above dancing around with Deleter::pointer is hacky, you are not alone. There was proposal to introduce a true generic RAII wrapper, unique_resource, into the standard. It is now under the std::experimental, so your vendor may have provided it already. There are also third-party implementations available.

Afterword

While it has limitations, std::unique_ptr is a great template for us to implement custom RAII classes. Doing so can enhance resource safety, simplify client code (encapsulation), and unify the resource-managing API to increase consistency and reduce cognitive load.