Previously I introduced the concept of scope-bound resource management, a very useful object-oriented concept available to us in C++.
Unfortunately, if you’re migrating from C to C++, you likely don’t have the luxury of converting all of your legacy code to modern C++. It would still be nice to utilize SBRM strategies with our C-style interfaces.
Taking a Look at C-style Resource Management
Let’s start by looking at the typical C-style resource management pattern. Many C libraries and interfaces utilize an initialization function, a stop
/release
/free
function, and also require the caller to maintain a handle. For example, consider the fopen
API:
FILE *fopen(const char *filename, const char *mode);
We supply fopen
with a filename and mode, and in return we get a file handle (FILE*
). This file handle is used by other file APIs. When we are done with our file, the handle must be passed to fclose
.
As we saw in the SBRM article, this is the perfect type of operation for utilizing SBRM. Luckily we have quite a simple way to utilize SBRM for managing resources from a C-style interface: C++ smart pointers.
Let’s take a look at how to make smart pointers work for our C-style resources, starting with handling the cleanup of our resource.
Specifying Custom Deleters
Both std::unique_ptr
and std::shared_ptr
allow us to specify custom deleters. By using the custom deleter functionality, we can put our C-based resource under management by a smart pointer and rely on SBRM to handle cleanup for us.
Whenever the pointer goes out of scope (or the reference count goes to 0), the release function will be called on the managed pointer.
std::unique_ptr
In most cases, you will want to utilize a std::unique_ptr
for your resources. A std::unique_ptr
requires minimal overhead and indicates that the resource can only have one owner at a time.
std::unique_ptr
calls delete
by default. To put our C-objects under SBRM we will need to specify a custom deleter. For a std::unique_ptr
, the type of the deleter must be specified as part of the pointer type.
Using our file example, we can simply create a std::unique_ptr
to place our FILE
resource under management and specify fclose
as the deleter function. Once our pointer goes out of scope, fclose will be called on the file automatically.
std::unique_ptr< FILE, decltype(fclose)>
fptr( fopen("test.txt", "r"), fclose);
Any function can really be supplied as a deleter. Perhaps you need a more complex operation to handle the release of your resource, or maybe you just want to print something out for debug purposes. Easy peasy:
//lambda deleter fn
auto file_deleter = [](FILE * x) {
printf("Closing file pointer %p\n", x);
fclose(x);
};
std::unique_ptr< FILE, decltype(file_deleter)>
fptr(fopen("test.txt", "r"), file_deleter);
std::shared_ptr
If your resource needs to be shared by multiple consumers at one time, you will want to consider std::shared_ptr
. Like std::unique_ptr
, you can specify a custom deleter. Unlike std::unique_ptr
, the deleter is not part of a std::shared_ptr
’s type:
std::shared_ptr<FILE> fptr(fopen("test.txt", "r"), fclose);
When the std::shared_ptr
reference count drops to 0, the custom deleter will be called on the managed pointer.
Providing a Resource Generation Function
It’s great that we can now utilize SBRM for our C-style resources, but this is certainly not a beautiful line of code:
std::unique_ptr< FILE, decltype(file_deleter)> fptr
(fopen("test.txt", "r"), file_deleter);
Wouldn’t it be better to utilize an API like this one?
auto my_file = fopen_unique("test.txt", "r");
Luckily, we can create a simple wrapper function and file type to make life easier:
// Unique file type
typedef std::unique_ptr<FILE, decltype(&fclose)> ufile_t;
ufile_t fopen_unique(const char * fname, const char * mode)
{
return {fopen(fname, mode), fclose};
}
We can also build a similar wrapper for creating our std::shared_ptr
:
// Shared file type
typedef std::shared_ptr<FILE> sfile_t;
sfile_t fopen_shared(const char * fname, const char * mode)
{
return {fopen(fname, mode), fclose};
}
A More Generic Approach Using Templates
Implementing a wrapper function to reduce C++ verbosity is a great strategy if you are only managing a single C-style resource. However, if we’re managing multiple resources, re-implementing wrappers for each type can become tedious. I’ve lifted a more generic approach from a StackOverflow article. The resource creation is a bit more verbose, but the approach is generic and reusable. Instead of using our fopen_unique
approach above, we would open a file like this:
auto ufile = acquire_unique_resource<FILE>(fopen, fclose, filename, "w");
We’ll call our generic acquire_unique_resource
method and template it on the type of resource we want to be managed (FILE
). Then we supply the acquisition function (fopen
) and the release function (fclose
). Any additional arguments supplied to the function will be forwarded your acquisition function (filename
and mode
).
Let’s take a look at the variadic template function implementation:
template<typename T,
typename AcquisitionFunc,
typename ReleaseFunc,
typename ...Args>
std::unique_ptr<T,ReleaseFunc> acquire_unique_resource(
AcquisitionFunc acquire, ReleaseFunc release, Args&&...args)
{
return {acquire(std::forward<Args>(args)...), release};
}
We can also create an acquire_shared_resource
function. This function returns a std::shared_ptr
and instead of a std::unique_ptr
:
template<typename T,
typename AcquisitionFunc,
typename ReleaseFunc,
typename ...Args>
std::shared_ptr<T> acquire_shared_resource(
AcquisitionFunc acquire, ReleaseFunc release, Args&&...args)
{
return {acquire(std::forward<Args>(args)...), release};
}
This pattern is usable for any C-style resource that utilizes an acquisition/initialization function and returns a pointer.
Putting It All Together
You can find a C-style SBRM example in the embedded-resources
Github repository. Start using SBRM to manage your C-style resources!
Further Reading
- Migrating from C to C++: Take Advantage of RAII/SBRM
- C++ Smart Pointers
- Ditch Your C-style Pointers for Smart Pointers
- StackOverflow: Using RAII To Manage Resources from a C-style API
Using C++ Without the Heap
Want to use C++, but worried about how much it relies on dynamic memory allocations? Our course provides a hands-on approach for learning a diverse set of patterns, tools, and techniques for writing C++ code that never uses the heap.
Learn More on the Course Page