Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

PrevUpHomeNext

Unique resource wrapper

Resource traits
Simplified resource traits
Comparison with unique_resource defined in C++ Extensions for Library Fundamentals
#include <boost/scope/unique_resource.hpp>

Boost.Scope provides a unique_resource class template. This is a wrapper for an arbitrary resource that represents exclusive ownership of the resource. The wrapper offers access to the owned resource and automatically calls a deleter function object to free the resource upon destruction. This is a generalization of std::unique_ptr, but while std::unique_ptr is only used to wrap pointers, unique_resource can wrap other types of resources as well.

A resource type must have the following properties to be compatible with unique_resource:

Note that if the resource type is a reference, the referenced object must be stored externally to the unique_resource wrapper and must remain valid for the entire duration of ownership by the wrapper. Users are not expected to access the resource object other than through the unique_resource wrapper. For example, it is not allowed to modify the resource object by setting it to an invalid state or explicitly freeing the resource circumventing the resource wrapper, as the wrapper will still invoke the deleter on the then-invalid resource.

The deleter must be a function object type that is callable on an lvalue of the resource type. The deleter must be copy-constructible. Although not strictly required, it is generally highly recommended that calling a deleter on a resource doesn't throw exceptions and is marked noexcept, as the deleter will often be called from the unique_resource destructor.

Like std::unique_ptr, unique_resource is not copyable and supports a number of other operations, such as move construction and assignment, reset, release and swap with the similar semantics. Some of the operations impose additional requirements on the resource and deleter types; such operations will not be available if those requirements aren't met. For example, swap is only supported if both the resource and deleter types are swappable, and at least one of those types is nothrow swappable (the last part is necessary to implement the strong exception guarantee for the swap operation). The requirements are listed for each operation in the unique_resource class reference.

[Tip] Tip

If the resource type is dereferenceable (i.e. supports unary operator*), unique_resource also provides operator* and operator->, making it act more like a smart-pointer.

Let's consider a usage example. Here, we need to compare the contents of two files; equal_files returns true if the files have equal contents and false otherwise.

// A deleter for POSIX-like file descriptors
struct fd_deleter
{
    void operator() (int fd) const noexcept
    {
        if (fd >= 0)
            close(fd);
    }
};

// Opens a file with the given filename for reading and returns wrapped file descriptor
boost::scope::unique_resource< int, fd_deleter > open_file(std::string const& filename)
{
    boost::scope::unique_resource< int, fd_deleter > file(open(filename.c_str(), O_RDONLY));
    if (file.get() < 0)
    {
        int err = errno;
        throw std::system_error(err, std::generic_category(), "Failed to open file " + filename);
    }

    return file;
}

// Reads contents of a file denoted by fd into buffer and returns the number of read bytes
std::size_t read_file(int fd, unsigned char* buf, std::size_t size)
{
    std::size_t total_read_size = 0;
    while (total_read_size < size)
    {
        ssize_t read_size = read(fd, buf + total_read_size, size - total_read_size);
        if (read_size < 0)
        {
            int err = errno;
            if (err == EINTR)
                continue;
            throw std::system_error(err, std::generic_category(), "Failed to write data");
        }

        if (read_size == 0)
        {
            // End of file
            break;
        }

        total_read_size += read_size;
    }

    return total_read_size;
}

// Compares contents of two files for equality
bool equal_files(std::string const& filename1, std::string const& filename2)
{
    // Create unique resource wrappers for files
    auto file1 = open_file(filename1.c_str());
    auto file2 = open_file(filename2.c_str());

    // Use them to read file contents
    constexpr std::size_t buf_size = 1024;
    unsigned char buf1[buf_size];
    unsigned char buf2[buf_size];
    while (true)
    {
        std::size_t size1 = read_file(file1.get(), buf1, buf_size);
        std::size_t size2 = read_file(file2.get(), buf2, buf_size);

        if (size1 != size2)
        {
            // File sizes are different
            return false;
        }

        if (std::memcmp(buf1, buf2, size1) != 0)
        {
            // File contents are different
            return false;
        }

        if (size1 < buf_size)
        {
            // Files have ended. At this point we know they have equal contents.
            break;
        }
    }

    return true;
}
[Note] Note

POSIX-like file descriptors are supported on Windows through non-standard APIs like _open, _close, _read, _write and others. It is often possible to port a program performing simple IO with regular files and file descriptors to Windows by simple renaming. However, here and below we will be using the standard POSIX nomenclature.

In this example, there are several points to note. First, the fd_deleter function object checks if the file descriptor is valid before passing it to close. This is necessary in this piece of code because we unconditionally initialize unique_resource object with the value returned by open, which may be -1 indicating an error. By default, unique_resource doesn't discriminate valid (i.e. allocated) resource values from invalid (i.e. unallocated), which means that from its perspective the value of -1 is a file descriptor that needs to be freed by calling the deleter on it. Therefore, the deleter must be prepared to handle resource values that are unallocated. Later on we will see how to mitigate this.

[Tip] Tip

Besides the resource value, you can specify the deleter object as the second argument of the unique_resource constructor. In fact, the second argument is mandatory if the deleter is not default-constructible or if its default constructor may throw. This is necessary to be able to call the deleter on the passed resource value, should the construction of either the resource or the deleter throw. This ensures that the resource doesn't leak in case if unique_resource initialization fails.

Second, we still need to check if opening the file succeeded before using it. Since unique_resource in this example is always constructed in allocated state, we have to check the file descriptor value for being negative. In order to access the resource value one can use the get method of unique_resource.

We can improve this example by making unique_resource initialization automatically check for the file descriptor validity. This will only require modifying fd_deleter and open_file, the rest of the code stays unchanged.

// A deleter for POSIX-like file descriptors
struct fd_deleter
{
    void operator() (int fd) const noexcept
    {
        close(fd);
    }
};

// Opens a file with the given filename for reading and returns wrapped file descriptor
boost::scope::unique_resource< int, fd_deleter > open_file(std::string const& filename)
{
    auto file = boost::scope::make_unique_resource_checked(
        open(filename.c_str(), O_RDONLY)
        -1,
        fd_deleter());
    if (!file.allocated())
    {
        int err = errno;
        throw std::system_error(err, std::generic_category(), "Failed to open file " + filename);
    }

    return file;
}

Here, the make_unique_resource_checked helper function performs the following:

  1. If the resource value, which is the first argument - the result of the open call, is equal to the invalid resource value, which is the second argument, creates and returns an unallocated unique_resource object.
  2. Otherwise, creates and returns an allocated unique_resource object that wraps the result of the open call.
  3. In both cases, the third argument is used to initialize the deleter in the returned unique_resource object.

Note that if open fails and returns -1, the deleter is guaranteed not to be called on this resource value - neither by make_unique_resource_checked nor by unique_resource, not even if unique_resource construction fails with an exception. This allows us to simplify fd_deleter and remove the check for negative fd values.

[Tip] Tip

If C++17 is supported, it is possible to use functor from Boost.Core to wrap raw functions like close into a function object that can be specified as a deleter in unique_resource. For example, fd_deleter from the example above could be replaced with boost::core::functor< close >.

Furthermore, since the returned unique_resource object now properly indicates whether the resource is allocated or not, we can use the allocated member function to test it instead of checking the resource value. This makes the code more readable.

[Note] Note

In order to avoid confusion with the unique_resource object being always allocated after construction with a resource value, it is recommended to always use either the make_unique_resource_checked factory function or, more preferably, resource traits, as described in the next section. Resource traits are the preferred solution as it makes unique_resource more efficient and less error-prone to use.

The unique_resource class template also supports an optional third template parameter, which can be used to specify a resource traits class. Resource traits provide unique_resource with additional knowledge about the resource features that allow for a more efficient implementation. This is useful when there is one or more resource value that is considered unallocated, that is such a value that does not identify an allocated resource that needs to be freed. For example, for pointer resource types, null is usually considered as the unallocated value, and for POSIX-like file descriptors, all negative values are unallocated values, since no valid file descriptor can be negative.

If Resource is the resource type specified in unique_resource template parameters then, if specified, resource traits must be a class type with the following public static member functions:

  • bool is_allocated(Resource const& r) noexcept - must return true if the resource value r is an allocated resource value and false otherwise.
  • R make_default() noexcept - must return a value such that std::is_constructible< Resource, R >::value && std::is_nothrow_assignable< Resource&, R >::value is true and constructing Resource from R produces an unallocated resource value that can be used to initialize the default-constructed unique_resource object.

Note that all listed member functions must be non-throwing. Given these definitions, calling is_allocated on the value returned by make_default must always return false.

When the conforming resource traits are provided, unique_resource behavior changes as follows:

  • unique_resource will no longer separately track whether it is in allocated state or not and instead will use is_allocated on the wrapped resource value for this. In particular, this means that constructing unique_resource from a resource value or calling reset with a resource value may now produce a wrapper in an unallocated state, if the resource value is an unallocated value.
  • Default constructor, reset with no arguments, move constructor and move assignment of unique_resource will use make_default to initialize the resource value in the unallocated wrapper (for move operations - in the move source).

Using the resource traits allow us to further improve the example given in the previous section. Again, our primary interest is open_file and unique_resource type usage, the rest of equal_files implementation remains unchanged.

// A deleter for POSIX-like file descriptors
struct fd_deleter
{
    void operator() (int fd) const noexcept
    {
        close(fd);
    }
};

// Resource traits for POSIX-like file descriptors
struct fd_resource_traits
{
    static bool is_allocated(int fd) noexcept
    {
        return fd >= 0;
    }

    static int make_default() noexcept
    {
        // Return any unallocated resource value
        return -1;
    }
};

// A shorthand type alias for unique file descriptor wrappers
using unique_fd = boost::scope::unique_resource< int, fd_deleter, fd_resource_traits >;

// Opens a file with the given filename for reading and returns wrapped file descriptor
unique_fd open_file(std::string const& filename)
{
    unique_fd file(open(filename.c_str(), O_RDONLY));
    if (!file.allocated())
    {
        int err = errno;
        throw std::system_error(err, std::generic_category(), "Failed to open file " + filename);
    }

    return file;
}

In this piece of code, we no longer need to use make_unique_resource_checked every time we open a file (or otherwise create a file descriptor), and therefore we don't have to duplicate the invalid file descriptor value or the deleter - this information is provided by fd_resource_traits and conveniently embedded in the unique_fd type. As before, if open fails and returns -1, the constructed unique_resource object will be in an unallocated state, meaning that we can use allocated accessor, and that the deleter will only be called if open succeeded.

[Tip] Tip

The fd_deleter, fd_resource_traits and unique_fd types presented in the examples above are provided by the library out of the box in [boost_scope_fd_resource_hpp] and boost/scope/unique_fd.hpp headers.

[Note] Note

Components described in this section require a C++17 compiler that supports auto non-type template parameters and fold expressions.

The library provides an unallocated_resource class template that can be used to generate resource traits for use with unique_resource when the resource satisfies the following constraints:

  • Resource values are allowed to be specified as non-type template parameters in C++. The exact set of types that meet this requirement depends on the C++ standard version being used. For example, integers, enumerations, pointers and lvalue references are supported since C++17. C++20 adds support for class types. Note that resource value initialization expression must be a constant expression. In particular, for pointers and references this means that the resource value initialization expression must refer to an object or function or, in case of pointers, produce a null pointer.
  • There is one or more unallocated resource values that can be individually listed. All other resource values represent allocated resources that need to be freed.
  • One of these unallocated resource values is considered the default.
  • Resource type supports move construction and assignment from the default value, as well as comparison for equality and inequality with unallocated values, and none of these operations throw exceptions.

When the above requirements are met, one can specify the unallocated resource values as non-type template parameters of unallocated_resource to generate resource traits for unique_resource. The first of the listed unallocated values is the default.

For example, let's consider resource traits definition for Windows handles.

struct handle_traits
{
    //! Returns the default resource value
    static HANDLE make_default() noexcept
    {
        return INVALID_HANDLE_VALUE;
    }

    //! Tests if \a res is an allocated resource value
    static bool is_allocated(HANDLE res) noexcept
    {
        return res != INVALID_HANDLE_VALUE && res != (HANDLE)NULL;
    }
};

Here, INVALID_HANDLE_VALUE is a special constant returned by some Windows API functions that indicates no allocated resource associated with the handle. However, other functions, like OpenProcess or OpenThread for example, return a NULL handle in case of errors, and we have to test for that value as well.

[Tip] Tip

For the curious readers, there is an article describing reasons for this inconsistency between different Windows APIs.

With unallocated_resource, the above resource traits could be reduced to this:

using handle_traits = boost::scope::unallocated_resource< INVALID_HANDLE_VALUE, (HANDLE)NULL >;
[Note] Note

Given that INVALID_HANDLE_VALUE is defined as (HANDLE)-1 in Windows SDK, and HANDLE is actually a pointer type, in pure C++ one should not be able to specify this value as a non-type template parameter because the pointer does not refer to an object. However, MSVC accepts this code as an extension. With other compilers, one would still have to implement proper resource traits similar to handle_traits in this case.

We can also use functor to wrap the CloseHandle Windows API function, which is used to free handles, to define the resource deleter. Then the complete definition of unique_resource would look like this:

using unique_handle = boost::scope::unique_resource<
    HANDLE,
    boost::core::functor< CloseHandle >,
    boost::scope::unallocated_resource< INVALID_HANDLE_VALUE, (HANDLE)NULL >
>;

The following sections provide comparison between unique_resource defined by C++ Extensions for Library Fundamentals TS and this library.

Boost.Scope supports an additional optional template parameter for resource traits. This allows unique_resource to become aware of the resource specifics, such as unallocated values and the default value, which is useful for usability and efficiency. Specifying resource traits makes the make_unique_resource_checked factory function unnecessary, as unique_resource itself is able to tell whether the resource is allocated upon construction. Additionally, this allows unique_resource to avoid storing an additional flag that indicates whether the resource is is in an allocated state and needs to be freed upon destruction.

Using resource traits may change meaning of some unique_resource APIs, which is, arguably, for the better. Consider the following example.

// POSIX file descriptor resource
std::experimental::unique_resource< int, int (*)(int) > fd(-1, &close);

Since the deleter in this case must be specified in constructor parameters (because the default constructor would create a value-initialized pointer to function, which is null and cannot be called), the user may be inclined to specify -1 for the file descriptor and expect that the constructed unique_resource is in an unallocated state. This assumption would be incorrect because this constructor always creates an allocated unique_resource, which means it will call close(-1) upon destruction. The correct way to construct fd in this case would be to use make_unique_resource_checked like so:

std::experimental::unique_resource< int, int (*)(int) > fd = std::experimental::make_unique_resource_checked(-1, -1, &close);

Alternatively, one can call release immediately after constructing the unique_resource. Although it should be noted that this approach is only valid if it is known that unique_resource construction cannot throw.

std::experimental::unique_resource< int, int (*)(int) > fd(-1, &close);
fd.release(); // mark the resource unallocated

A similar problem exists with the reset member function.

fd.reset(-1);

Contrary to expectation, this call does not leave fd in unallocated state and also causes close(-1) to be called on fd destruction. The correct way to do this is to call reset without parameters or use make_unique_resource_checked.

fd.reset();
// or:
fd = std::experimental::make_unique_resource_checked(-1, -1, &close);
[Note] Note

It may look like calling reset(-1) is an artificial example that doesn't happen in practice. It is not unusual for one to want to combine another call with reset directly (e.g. fd.reset(open(...))) and expect this to work correctly. However, since open may return -1 in case of error, we will have the problem described above.

Specifying resource traits changes meaning of the above examples. Both the constructor and reset from an unallocated resource value produce a unique_resource object in an unallocated state.

boost::scope::unique_resource< int, int (*)(int), boost::scope::fd_resource_traits > fd(-1, &close);
assert(!fd.allocated());

fd.reset(-1);
assert(!fd.allocated());
[Tip] Tip

fd_resource_traits is provided by Boost.Scope.

Another benefit of resource traits is that the default resource value can be different from the value-constructed one. This can be useful if the resource type is not default-constructible (for example, if it is a reference type) or the value-constructed resource is not an unallocated value. For example, fd_resource_traits specifies -1 as the default value for POSIX file descriptors because the value of 0 is a valid allocated file descriptor.

There is no direct replacement for this feature the Library Fundamentals TS, although make_unique_resource_checked and release can be used to work around the limitation in some cases, as shown above.

Unlike Library Fundamentals TS, Boost.Scope does not consider pointers to functions default-constructible for the purpose of deleters specified in unique_resource template parameters. For example:

std::experimental::unique_resource< int, int (*)(int) > fd1; // compiles, creates unusable unique_resource object
boost::scope::unique_resource< int, int (*)(int) > fd2; // fails to compile, unique_resource is not default-constructible

This is because the default- or value-constructed pointer to function is not callable, as it is garbage or null pointer, respectively. unique_resource does not provide a way to modify the deleter after construction, other than by move-assigning another unique_resource object, which means the default-constructed unique_resource object would be unusable and potentially cause undefined behavior on reset or destruction.

Boost.Scope does allow omitting the deleter from constructor arguments when it truly is default-constructible and the default constructor doesn't throw exceptions, while the TS requires both the resource value and the deleter to be specified.

std::experimental::unique_resource< int, boost::scope::fd_deleter > fd1(10); // fails to compile, deleter is missing in constructor arguments
boost::scope::unique_resource< int, boost::scope::fd_deleter > fd2(10); // ok, default-constructs fd_deleter
[Tip] Tip

fd_deleter is provided by Boost.Scope.

[Note] Note

The requirement for the deleter default construction to be non-throwing is to ensure that the deleter can be invoked on the resource in case if unique_resource construction fails with an exception. This prevents leaking the resource if its construction throws, or the deleter's constructor throws.

Boost.Scope allows omitting the resource value in unique_resource constructor arguments, when the deleter must be specified. default_resource keyword can be used as a placeholder for the resource value in this case:

// Creates an unallocated unique_resource with the specified deleter
boost::scope::unique_resource< int, int (*)(int) > fd(boost::scope::default_resource, &close);

Note that in the example above, even though the resource value is value-initialized (i.e. zero), the fd is in an unallocated state and will not call the deleter on destruction. default_resource also works with resource traits:

boost::scope::unique_resource< int, int (*)(int), boost::scope::fd_resource_traits > fd(boost::scope::default_resource, &close);

In the above case, the fd object will use the default value specified by the resource traits (i.e. -1, in case of fd_resource_traits) to initialize its stored resource object. Again, the fd object is in an unallocated state after construction.

Library Fundamentals TS specifies that unique_resource move constructor must deallocate the resource if the resource is nothrow move-constructible and the deleter is not and the deleter's copy constructor throws and exception. This is a case when unique_resource is half-constructed: the resource has been moved into the object being constructed (i.e. is no longer stored in the source object) but the deleter fails to copy-construct (i.e. it only exists in the source object). The behavior described in the TS ensures that the resource doesn't leak, but it leaves the source unique_resource unallocated in case of exception, which means the move constructor only maintains basic exception guarantee. Boost.Scope in this case leaves the source unique_resource in its original state, which also guarantees that the resource doesn't leak, but provides a strong exception guarantee.

One glaring omission in the Library Fundamentals TS is that unique_resource doesn't provide a way to test whether the resource is in an allocated state. Boost.Scope rectifies this by providing allocated() method, as well as contextual conversion to bool.

boost::scope::unique_resource< int, int (*)(int), boost::scope::fd_resource_traits > fd(-1, &close);
assert(!fd.allocated());
assert(!fd);

fd.reset(10);
assert(fd.allocated());
assert(!!fd);
[Note] Note

The contextual conversion to bool in unique_resource does not test the resource value and only tests whether the resource is allocated. This is consistent with other components, such as std::optional, for example.

Library Fundamentals TS defines unique_resource to be a move-constructible and move-assignable type, and through this property it supports the standard std::swap algorithm. However, there are two issues with this approach:

  • It prevents swapping algorithms specialized for the resource and deleter types from being used. Even if swap is specialized, the generic algorithm involving moving or copying the resource and deleter objects is used.
  • The generic swap implementation only provides basic exception guarantee. The algorithm move-constructs a copy of one of the source objects and then performs two move-assignments. If either of the assignments fail, one of the unique_resource objects will be left unallocated. It is also worth noting that the TS defines unique_resource move constructor to be potentially destructive. If the resource type move constructor is non-throwing and the deleter copy-constructor throws, the deleter is called on the move-constructed resource object to avoid leaking it. In the context of std::swap this means that even if the move constructor fails, it will leave the source object unallocated rather than unmodified.

Boost.Scope provides native support for swapping, both as a member and non-member function swap. Swapping is supported if at least one of the resource or deleter types support non-throwing swap (either default or specialized algorithm) and provides strong exception guarantee. unique_resource swap operation is non-throwing if both resource and deleter types support non-throwing swap.

boost::scope::unique_resource< int, boost::scope::fd_deleter, boost::scope::fd_resource_traits > fd1, fd2;
fd1.swap(fd2);
swap(fd1, fd2); // found via ADL

Library Fundamentals TS unique_resource supports operator*() and operator->() for pointer resource types. Boost.Scope extends this support for any resource types that support dereferencing.

// Resource object type
struct my_object
{
    void foo();
};

// Manager for my_object instances
class my_object_manager
{
private:
    // List of allocated objects
    using my_object_list = std::list< my_object >;

    // Resource handle that refers to a my_object element in the list
    class my_handle
    {
    private:
        my_object_list* m_list;
        my_object_list::iterator m_it;

    public:
        my_handle() noexcept : m_list(nullptr), m_it() {}
        explicit my_handle(my_object_list& list, my_object_list::iterator it) noexcept :
            m_list(&list), m_it(it)
        {
        }

        my_object_list::iterator operator-> () const noexcept  1
        {
            return m_it;
        }

        my_object& operator* () const noexcept
        {
            return *m_it;
        }

        my_object_list* get_list() const noexcept
        {
            return m_list;
        }

        my_object_list::iterator get_iterator() const noexcept
        {
            return m_it;
        }
    };

    // Resource deleter
    struct my_handle_deleter
    {
        void operator() (my_handle const& res) const noexcept
        {
            res.get_list()->erase(res.get_iterator());
        }
    };

    // Resource traits
    struct my_handle_traits
    {
        static my_handle make_default() noexcept
        {
            return my_handle();
        }

        static bool is_allocated(my_handle const& res) noexcept
        {
            return res.get_list() != nullptr && res.get_iterator() != res.get_list()->end();
        }
    };

public:
    // Unique resource wrapper for my_object instances
    using my_object_handle = boost::scope::unique_resource< my_handle, my_handle_deleter, my_handle_traits >;

private:
    my_object_list m_objects;

public:
    // Allocates a new object and returns a handle to it
    my_object_handle allocate_object()
    {
        return my_object_handle(my_handle(m_objects, m_objects.insert(m_objects.end(), my_object())));
    }
};

void allocate_and_use_object(my_object_manager& mgr)
{
    my_object_manager::my_object_handle handle = mgr.allocate_object();
    if (handle)
        handle->foo(); // invokes my_object::foo on the allocated object
}

1

The operator relies on operator-> chaining.

In the above example, we're using unique_resource to reference objects created and maintained by my_object_manager. The my_handle class is the resource type and supports accessing my_object members through operator-> and operator*. This enables operator-> and operator* in unique_resource, which allows the code that uses unique_resource to access my_object members.

With Library Fundamentals TS unique_resource, this is possible to emulate by accessing the stored resource object via the unique_resource::get accessor and dereferencing that.


PrevUpHomeNext