Resource Management with Explicit Template Specializations

Resource Management with Explicit Template Specializations

By Pavel Frolov

Overload, 23(126):18-22, April 2015


RAII is a useful idiom. Pavel Frolov presents a powerful extension using explicit template specialization.

RAII is one of the most important and useful C++ idioms. RAII efficiently relieves the programmer of manual resource management and is a must for writing exception-safe code. Perhaps, the most ubiquitous usage of RAII is dynamic memory management with smart pointers, but there are a plenty of other resources for which it can be applied, notably in the world of low-level libraries. Examples are Windows API handles, POSIX file descriptors, OpenGL primitives, and so on.

Applying RAII: available options

There are several choices we could make when deciding to implement an RAII wrapper for a resource of some kind:

  • write a specific wrapper for that particular resource type;
  • use a standard library smart pointer with custom deleter (e.g., std::unique_ptr<Handle, HandleDeleter> );
  • implement a generic one ourselves.

The first option, writing a specific wrapper, may seem reasonable at the beginning, and in fact, is a good starting point. The simplest RAII wrapper may look something like Listing 1.

class ScopedResource {
public:
  ScopedResource() = default;
  explicit ScopedResource(Resource resource)
    : resource_{ resource } {}
  ScopedResource(const ScopedResource&) = delete;
  ScopedResource& operator=
    (const ScopedResource&)
    = delete;
  ~ScopedResource() { DestroyResource(resource_); }
operator const Resource&() const {
  return resource_; }  

private:
  Resource resource_{};
};
			
Listing 1

However, as your code base grows in size, so does the number of resources. Eventually you’ll notice that most of resource wrappers are quite similar, usually the only difference between them is the clean-up routine. This causes error-prone copy/paste-style code reuse. On the other hand, this is a great opportunity for generalization , which leads us to the second option: smart pointers.

The smart pointer class template is a generic solution to resource management. Even so, it has its own drawbacks, which we will discuss shortly. As their name suggests, smart pointers were designed mainly for memory management and their usage with other kinds of resources is often at least inconvenient. Let’s look at the smart pointer option in more detail.

Why smart pointers are not smart enough

Consider the code in Listing 2.

#include <memory>

// From low-level API.
using Handle = void*;
Handle CreateHandle() { Handle h{ nullptr };
  /*...*/ return h; }
void CloseHandle(Handle h) { /* ... */ }

struct HandleDeleter {
  void operator()(Handle h) { CloseHandle(h); }
};
using ScopedHandle = std::unique_ptr<Handle,
                     HandleDeleter>;
int main() {
  // error: expected argument of type void**
  ScopedHandle h{ CreateHandle() };
}
			
Listing 2

Why is the ScopedHandle constructor expecting an argument of type void** ? Recall, that smart pointers were designed primarily for pointer types: std::unique_ptr<int> actually manages int* . Similarly std::unique_ptr<Handle> manages Handle* which is an alias for void** in our example. How can we work around this? First, we could use the std::remove_pointer metafunction:

  using ScopedHandle =
    std::unique_ptr<std::remove_pointer_t<Handle>,
    HandleDeleter>; 

Second, we could use an obscure feature of the smart pointer deleter: if there exists a nested type named pointer , then this type is used by unique_ptr as a managed pointer type:

  struct HandleDeleter {
    using pointer = Handle;
    void operator()(Handle h) { CloseHandle(h); }
  };
  using ScopedHandle = std::unique_ptr<Handle,
                       HandleDeleter>;

As you can see, neither of these solutions is as user-friendly as we want them to be, but the main problem is another. Smart pointer forces you to make assumptions about Handle type. But handle is meant to be an opaque descriptor, the actual definition of handle is an implementation detail of which the user is not required to be aware.

There is another, more serious problem with smart pointer approach (see Listing 3).

#include <memory>
using Handle = int;
Handle CreateHandle() {
  Handle h{ -1 }; /*...*/ return h; }
void CloseHandle(Handle h) { /* ... */ }

struct HandleDeleter {
  using pointer = Handle;
  void operator()(Handle h) { CloseHandle(h); }
};
using ScopedHandle = std::unique_ptr<Handle,
                     HandleDeleter>;
int main() {
  // Error: type mismatch: "int" and
  // "std::nullptr_t".
  ScopedHandle h{ CreateHandle() };
}
			
Listing 3

In practice, the code above may work without problems with some of std::unique_ptr implementations, but in general this is not guaranteed and definitely is not portable.

The reason for an error in this case is a violation of the NullablePointer concept [ NullablePointer ] by the managed type. In a nutshell, the model of the NullablePointer concept must be pointer-like type, comparable to nullptr . Our Handle , defined as an alias to int , is no such thing. As a consequence, we can’t use unique_ptr for something like POSIX file descriptors or OpenGL GLuint handles.

There is a workaround, though. We can define an adaptor for Handle which fulfils the requirements of NullablePointer , but writing a wrapper for a wrapper is way too much.

Yet another smart pointer issue is related to convenience of use. Consider idiomatic usage of a hypothetical Bitmap resource (Listing 4).

// Graphics API.
bool CreateBitmap(Bitmap* bmp) {
  /*...*/
  return true;
}

bool DestroyBitmap(Bitmap bmp) {
  /* ... */
  return true;
}

bool DrawBitmap(DeviceContext ctx, Bitmap bmp) {
  /* ... */
  return true;
}
...

// User code.
DeviceContext ctx{};
Bitmap bmp{};
CreateBitmap(&bmp);
DrawBitmap(ctx, bmp);
			
Listing 4

Now compare this with the usage of std::unique_ptr for managing Bitmap (Listing 5).

struct BitmapDeleter {
  using pointer = Bitmap;
  void operator()(Bitmap bmp) {
    DestroyBitmap(bmp); } };
  using ScopedBitmap = std::unique_ptr<Bitmap,
                       BitmapDeleter>;
...
DeviceContext ctx{};
Bitmap tmp;
CreateBitmap(&tmp);
ScopedBitmap bmp{ tmp };
DrawBitmap(ctx, bmp.get());
			
Listing 5

As you can see, the ScopedBitmap is more awkward to use. In particular, it can’t be passed directly to functions designed for Bitmap .

Considering the above, let’s move to the third option: implementing an RAII wrapper ourselves.

Implementation

The implementation presented below is using a different approach to clean-up routine than standard library smart pointers. It takes advantage of an ability to selectively specialize non-template members of class template [ Template Specialization ]. (See Listing 6.)

#include <cassert>
#include <memory> // std::addressof

template<typename ResourceTag,
  typename ResourceType>
class Resource {

public:
  Resource() noexcept = default;
  explicit Resource(ResourceType resource)
    noexcept : resource_{ resource } {}
  Resource(const Resource&) = delete;
  Resource& operator=(const Resource&) = delete;
  Resource(Resource&& other) noexcept
    : resource_{ other.resource_ } 
    { other.resource_ = {}; }

  Resource& operator=(Resource&& other) noexcept {
    assert(this != std::addressof(other));
    Cleanup();
    resource_ = other.resource_;
    other.resource_ = {};
    return *this;
  }

  ~Resource() { Cleanup(); }
  operator const ResourceType&() const noexcept {
    return resource_;
  }

  ResourceType* operator&() noexcept {
    Cleanup();
    return &resource_;
  }

private:
  // Intentionally undefined - must be 
  // explicitly specialized.
  void Cleanup() noexcept;
  ResourceType resource_{};
};
			
Listing 6

First, some minor design points.

  • The class is noncopyable , but movable , thus, it provides sole ownership semantic (just like std::unique-ptr ). One can provide shared ownership counterpart (akin to std::shared_ptr ) if needed.
  • Taking into account that most ResourceType arguments are simple resource handles (like void* or int ), the class methods are defined noexcept .
  • Overloading operator& is a questionable (if not bad) design decision. Nevertheless, I decided to do it in order to facilitate the usage of the class with factory functions of the form void CreateHandle(Handle* handle) .

Now to the core. As you can see, the Cleanup method which is the cornerstone of our RAII wrapper is left undefined. As a result, an attempt to instantiate such a class will lead to an error. The trick is to define an explicit specialization of Cleanup for each particular resource type. For example:

  // Here "FileId" is some OS-specific file
  // descriptor Type which must be closed with
  // CloseFile function.
  using File = Resource<struct FileIdTag, FileId>;
  template<> void File::Cleanup() noexcept {
    if (resource_)
      CloseFile(resource_);
  }

Now we can use our class to wrap FileId objects:

  {
    File file{ CreateFile(file_path) };
    ...
  } // "file" will be destroyed here

You can think of the Cleanup declaration inside Resource as a ‘compile-time pure virtual function’. Similarly, explicit specialization of Cleanup for FileId is a concrete implementation of such a function.

What’s the deal with ResourceTag?

You may wonder, why do we need a ResourceTag template parameter which is used nowhere? It solves two purposes.

First is type-safety . Imagine two different resource types, say Bitmap and Texture , both of which are defined as type aliases for void* . Without the tag parameter, the compiler simply couldn’t detect the nasty bug in Listing 7.

using ScopedBitmap = Resource<Bitmap>;
using ScopedTexture = Resource<Texture>;
void DrawBitmap(DeviceContext& ctx,
                ScopedBitmap& bmp){
  /* ... */
}

int main() {
  DeviceContext ctx;
  ScopedBitmap bmp;
  ScopedTexture t;
  // Passing texture to function expecting bitmap.
  // Compiles OK.
  DrawBitmap(ctx, t);
}
			
Listing 7

With the help of the tag, however, the compiler can detect the error (Listing 8).

using ScopedBitmap = Resource<struct BitmapTag,
                     Bitmap>;
using ScopedTexture = Resource<struct TextureTag,
                      Texture>;

int main() {
  DeviceContext ctx;
  ScopedBitmap bmp;
  ScopedTexture t;
  DrawBitmap(ctx, t);  // error: type mismatch
}
			
Listing 8

The second purpose of the tag: it allows us to define Cleanup specializations for conceptually different resources having the same C++ type. Once again, imagine that our Bitmap resource requires a DestroyBitmap function while Texture requires DestroyTexture . Without tag parameters, ScopedBitmap and ScopedTexture would be the same type (recall that both Bitmap and Texture are in fact void* in our example), preventing us from defining specialized clean-up routines for each of them.

Speaking about the tag, the following expression may seem odd-looking to some:

  using File = Resource<struct FileIdTag, FileId>; 

In particular, I’m talking about the usage of struct FileIdTag as a template argument. Let’s see the equivalent expression, the meaning of which I bet is clear to those familiar with tag dispatching [ Tag Dispatching ]:

  struct FileIdTag{};
  using File = Resource<FileIdTag, FileId>;

Conventional tag dispatching makes use of function overloading with the argument of tag type being an overload selector. The tag is passed to the overloaded function by value, hence, tag type must be a complete type. In our case however, no function overloading is taking place. The tag is used only as a template argument to facilitate explicit specialization. Taking into account that C++ permits incomplete types as template arguments, we can replace tag type definition with a declaration:

  struct FileIdTag;
  using File = Resource<FileIdTag, FileId>;

Now, considering that FileIdTag is needed only inside the type alias declaration, we can move it directly into the place of usage:

  using File = Resource<struct FileIdTag, FileId>; 

Making an explicit specialization requirement a little more explicit

If the user fails to provide an explicit specialization for the Cleanup method, he/she will not be able to build the program. This is by design. However, there are two usability issues involved:

  • the error is reported at link-time, while it is preferable (and possible) to detect it much earlier, at compile-time;
  • the error message gives the user no clue about the actual problem and the way solve it.

Let’s try to fix it with the help of static_assert :

  void Cleanup() noexcept {
    static_assert(false,
    "This function must be explicitly "
    "specialized.");
  }

Unfortunately, it won’t work as expected: the assertion may produce an error even though the primary Cleanup method is never instantiated . The reason is the following: the condition inside static_assert does not depend in any way on our class template parameters, therefore, the compiler can evaluate the condition even before attempting to instantiate the template.

Knowing that, the fix is simple: make the condition dependent on template parameter(s) of the class template. We could do this by writing a compile-time member function which unconditionally produces a constant of the value false :

  static constexpr bool False() noexcept { 
    return false; }

  void Cleanup() noexcept {
    static_assert(False(),
    "This function must be explicitly "
    "specialized.");
  }

Thin wrappers vs. full-fledged abstractions

The RAII-wrapper template presented provides a thin abstraction dealing strictly with resource management. One may argue, why bother using such a wrapper instead of implementing a proper high-level abstraction in the first place? As an example, consider writing a bitmap class from scratch (see Listing 9).

class Bitmap {
public:
  Bitmap(int width, int height);
  ~Bitmap(){};
  int Width() const;
  int Height() const;
  Colour PixelColour(int x, int y) const;
  void PixelColour(int x, int y, Colour colour);
  DC DeviceContext() const;
  /* Other methods... */

private:
  int width_{};
  int height_{};
  // Raw resources.
  BITMAP bitmap_{};
  DC device_context_{};
};
			
Listing 9
Gotchas

A couple of gotchas to watch for when defining explicit template specializations:

  • explicit specialization must be defined in the same namespace as the primary template (in our case, the Resource class template);
  • an explicit specialization function definition residing in a header file must be inline : remember, the explicit specialization is a regular function, not a template anymore.

To see why such a design is a bad idea in general, let’s write a constructor for the Bitmap class (Listing 10).

Bitmap::Bitmap(int width, int height)
  : width_{ width }, height_{ height } {

  // Create bitmap.
  bitmap_ = CreateBitmap(width, height);
  if (!bitmap_)
    throw std::runtime_error{
      "Failed to create bitmap." };

  // Create device context.
  device_context_ = CreateCompatibleDc();
  if (!device_context_)
    // bitmap_ will be leaked here!
    throw std::runtime_error{
      "Failed to create bitmap DC." };

  // Select bitmap into device context.
  // ...
}
			
Listing 10

As you can see our class is actually managing two resources: the bitmap itself and the corresponding device context (this example is inspired by the Windows GDI, where a bitmap must be backed up by an in-memory device context for most of the drawing operations and for the sake of interoperability with modern graphics APIs). And here goes the problem: if the device_context_ initialization fails, the bitmap_ will be leaked!

On the other hand, consider the equivalent code with the usage of scoped resources (Listing 11).

using ScopedBitmap = Resource<struct BitmapTag,
                     BITMAP>;
using ScopedDc = Resource<struct DcTag, DC>;
...
Bitmap::Bitmap(int width, int height) 
  : width_{ width }, height_{ height } {

  // Create bitmap.
  bitmap_ = ScopedBitmap{
    CreateBitmap(width, height) };
  if (!bitmap_)
    throw std::runtime_error
      { "Failed to create bitmap." };

  // Create device context.
  device_context_ = ScopedDc
    { CreateCompatibleDc() };
  if (!device_context_)
    // Safe: bitmap_ will be destroyed in case of
    // exception.  
    throw std::runtime_error
      { "Failed to create bitmap DC." };

  // Select bitmap into device context.
  // ...
}
			
Listing 11

This example leads us to the following guideline: do not keep more than one unmanaged resource as a class member . Better consider applying RAII to each of the resources, and then use them as building blocks for a more high-level abstractions. This approach both ensures exception safety and code reuse (you can recombine those building block as you wish in the future without the fear of introducing resource leaks).

More examples

In Listing 12, you can see some real-world examples of useful specializations for Windows API objects. Windows API is chosen, because it provides many opportunities for RAII application. The examples are self-explanatory enough; no Windows API knowledge is required.

// Windows handle.
using Handle = Resource<struct HandleTag, HANDLE>;
template<> void Handle::Cleanup() noexcept {
  if (resource_ && 
      resource_ != INVALID_HANDLE_VALUE)
    CloseHandle(resource_);
}

// WinInet handle.
using InetHandle 
  = Resource<struct InetHandleTag, HINTERNET>;
template<> void InetHandle::Cleanup() noexcept {
  if (resource_)
    InternetCloseHandle(resource_);
}

// WinHttp handle.
using HttpHandle 
  = Resource<struct HttpHandleTag, HINTERNET>;
template<> void HttpHandle::Cleanup() noexcept {
  if (resource_)
    WinHttpCloseHandle(resource_);
}

// Pointer to SID.
using Psid = Resource<struct PsidTag, PSID>;
template<> void Psid::Cleanup() noexcept {
  if (resource_)
    FreeSid(resource_);
}

// Network Management API string buffer.
using NetApiString 
  = Resource<struct NetApiStringTag, wchar_t*>;
template<> void NetApiString::Cleanup() 
  noexcept {
  if (resource_ && NetApiBufferFree(resource_) 
    != NERR_Success) {
    // Log diagnostic message in case of error.
  }
}

// Certificate store handle.
using CertStore 
  = Resource<struct CertStoreTag, HCERTSTORE>;
template<> void CertStore::Cleanup() noexcept {
  if (resource_)
    CertCloseStore(resource_,
                   CERT_CLOSE_STORE_FORCE_FLAG);
}
			
Listing 12

Comparing with unique_resource from N3949

The limitations of smart pointers as a generic resource management tool discussed earlier have led to development of standard proposal N3949 [ N3949 ]. N3949 suggests a unique_resource_t class template similar to the one presented in the article but with a more conventional approach to the clean-up routine (i.e., in the vein of std::unique_ptr ) – see Listing 13.

template<typename Resource, typename Deleter>
class unique_resource_t {
  /* … */
};

// Factory.
template<typename Resource, typename Deleter>
unique_resource_t<Resource, Deleter>
unique_resource(Resource&& r, Deleter d) noexcept
{
  /* … */
}
...

// Usage (predefined deleter).
struct ResourceDeleter {
  void operator()(Resource resource) 
     const noexcept {
    if (resource)
      DestroyResource(resource);
  }
};

using ScopedResource =
  unique_resource_t<Resource, ResourceDeleter>;
ScopedResource r{ CreateResource(),
  ResourceDeleter{} };

// Alternative usage (in-place deleter definition).
auto r2{ unique_resource(
  CreateResource(),
  [](Resource r){ if (r) DestroyResource(r); })
};
			
Listing 13

As you can see, unique_resource_t uses a clean-up routine per resource instance , while the Resource class utilizes a clean-up routine per resource type approach. Conceptually, a clean-up routine is more a property of a resource type rather than instance (this is obvious from most of the real-world usage of RAII wrappers). Consequently, it becomes tedious to specify clean-up routine during each and every resource creation. On rare occasions, however, such a flexibility can be useful. As an example, consider the clean-up function which takes a policy flag to control the deletion of resource, such as the CertCloseStore Windows API function presented earlier in the examples section.

Speaking about the amount of code needed to define a resource wrapper, there is not much difference between Resource and unique_resource_t . Personally, I find function specialization definition to be more elegant than functor definition (i.e., struct with operator() ). For unique_resource_t we could also use in-place lambda instead, as shown above, but this quickly becomes inconvenient as we need to create resources in more than one place in the code (the lambda definition must be repeated then). On the other hand, passing callable objects in constructors to provide custom logic is widely used in C++, while defining explicit specializations may seem more exotic to most programmers.

Conclusion

The RAII wrapper presented in the article resolves most of the shortcomings of standard library smart pointers for managing resources of types other than memory. To be specific:

  • non-obvious declaration syntax for pointer type aliases;
  • limited support for non-pointer types;
  • awkward usage of managed resources with low-level APIs in comparison to unmanaged ones.

We have also become acquainted with a simple but interesting static polymorphism technique based on the usage of explicit template specialization. Historically, explicit template specialization has had the fame of an advanced language feature aimed mainly towards library implementers and experienced users. As you can see however, it can play a much more prominent role of a core abstraction mechanism on par with virtual functions, rather than being merely a helpful utility in a library implementer’s toolbox. I am convinced that the full potential of this feature has yet to be unlocked.

Code available at https://goo.gl/cK46xF

References

[N3949] http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2014/n3949.pdf

[NullablePointer] http://en.cppreference.com/w/cpp/concept/NullablePointer

[Tag Dispatching] http://www.boost.org/community/generic_programming.html#tag_dispatching

[Template Specialization] http://en.cppreference.com/w/cpp/language/template_specialization






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.