Fit RAII - revisited
Wrapping C API calls, improving the “fit RAII”.
Introduction
In previous articles I’ve described several idioms of wrapping C APIs such as classic RAII, slim RAII and fat RAII. Then I’ve looked at an industry quality wrapper. In this article I’m revisiting and improving this “fit RAII” version, that is suitable for larger projects.
Code
cpp_util_lib/raii_with_invalid_value.h
First let’s create a class template to deal with resources that have a invalid
value. For this simple example is not strictly necessary, because we’ll only
use it once with FILE *
.
It’s a RAII class with the main purpose of closing the handle to clean up the resource on destructor, if it does not have the invalid value.
Other similar templates can be imagined for situations like where there is no
invalid value and we need an extra bool
, or for the case where additional
data is required to clean up the resource.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#pragma once
namespace cpp_util
{
template<typename Traits>
// requires Traits defines
// - a handle type - the type of the handle
// - a function static void close_handle(handle) noexcept - the function to
// close the handle
// - and an invalid_value constant - the special value indicating "no handle"
class raii_with_invalid_value
{
public:
using handle = typename Traits::handle;
private:
handle h_;
public:
raii_with_invalid_value() noexcept :
h_{ Traits::invalid_value }
{
}
explicit raii_with_invalid_value(handle h) noexcept :
h_{ h }
{
}
~raii_with_invalid_value()
{
if (is_valid())
{
Traits::close_handle(h_);
}
}
raii_with_invalid_value(const raii_with_invalid_value &) = delete;
raii_with_invalid_value & operator=(const raii_with_invalid_value &) = delete;
raii_with_invalid_value(raii_with_invalid_value && other) noexcept :
h_{ other.h_ }
{
other.h_ = Traits::invalid_value;
}
raii_with_invalid_value & operator=(raii_with_invalid_value && other) noexcept
{
handle tmp = other.h_;
other.h_ = Traits::invalid_value;
if (is_valid())
{
Traits::close_handle(h_);
}
h_ = tmp;
return *this;
}
bool is_valid() const noexcept
{
return h_ != Traits::invalid_value;
}
explicit operator bool() const noexcept
{
return is_valid();
}
handle get() const noexcept
{
return h_;
}
handle & handle_reference() noexcept
{
return h_;
}
handle * handle_pointer() noexcept
{
return &h_;
}
void reset(handle h = Traits::invalid_value) noexcept
{
if (is_valid())
{
Traits::close_handle(h_);
}
h_ = h;
}
handle release() noexcept
{
handle tmp = h_;
h_ = Traits::invalid_value;
return tmp;
}
};
}
The constructor taking a handle is explicit
so that we don’t accidentally
take the ownership of the handle.
The class is moveable and has a default constructor which allows for scenarios where it’s default created without a handle value and the handle is provided later. The destructor then has to check for a valid handle before cleaning the resource.
The class is not copyable because the destructor would be called twice for both the original and the copy. This does not make sense for most resources e.g. it makes sense to copy the file contents, but not the file handle for independent usage. Such a copy for a file handle is either not possible or possible but undesirable most of the time.
The get
, reset
and release
methods create some similarities in usage to
smart pointers.
handle_reference
and handle_pointer
are provided for direct access to the
handle. Such access is a bit dangerous, but sometimes useful (hence the longer
names), less dangerous than using naked handles for C APIs which e.g. use the
return value for errors and provide created handles via a pointer to handle
argument.
The operator bool
checks for is_valid()
, it is explicit
, limiting it’s
use e.g. to if
tests.
All the methods are noexcept
either explicitly or implicitly, which is useful
when doing low level interactions with C code.
Why use raii_with_invalid_value
instead of std::unique_ptr
?
Unlike std::unique_ptr
it handles the case where the invalid value is not
nullptr
. It provides the is_valid()
to check for validity, which checks
against the correct invalid value.
operator*
and operator->
, provided by std::unique_ptr
, but not
raii_with_invalid_value
, don’t usually make sense for handles. Handles are
sometimes opaque types or they are not even pointers (such as the handle type
for POSIX open
which is int
). Instead raii_with_invalid_value
provides
additional functions such as handle_pointer
that are useful when dealing with
handles as explained above.
Equality is not implemented, because it does not make sense. The handles are
often opaque and each valid handle has a different value, so the only equal
values will be those of invalid handles. Equality is linked to copying. The
standard implemented equality for std::unique_ptr
. That is arguable a design
mistake in std::unique_ptr
. Equality there does not make sense for the same
reason, only nullptr
pointers will compare equal. Note that equality makes
sense for std::shared_ptr
, which has copy, it answers the question “do these
two pointers point to the same thing?”, which is possible for
std::shared_ptr
.
Another reason is that because of the traits mechanism it gives an option to
have different handle wrapper types even when all the characteristics are the
same: same handle type, same invalid value, same closing function. E.g.
HANDLE
(also SC_HANDLE
) is used for many APIs in Windows, not always
meaning the same thing (files, threads, events, semaphores etc.)
But fundamentally it’s semantics. While you might sometimes just about use
std::unique_ptr
, raii_with_invalid_value
is better suited tool to deal with
much more of the usual variation in handle behaviour in C APIs and
refer to all such handle wrappers by the same name, leaving std::unique_ptr
for the role of managing single owned heap allocated memory.
cpp_util_lib/handle_arg.h
Second let’s create a class template that we can use as a function argument, for when the function accepts either a raw handle or the RAII class owning the handle.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#pragma once
namespace cpp_util
{
template<typename RaiiClass>
class handle_arg
{
using handle = typename RaiiClass::handle;
handle h_;
public:
handle_arg(handle h) noexcept :
h_{ h }
{
}
handle_arg(const RaiiClass & raii) noexcept :
h_{ raii.get() }
{
}
handle_arg(RaiiClass &&) noexcept = delete;
operator handle() const noexcept
{
return h_;
}
};
}
This class is a bit like std::string_view
that can be used as an argument for
a function that accepts either a string literal or a std::string
, but unlike
std::string_view
that can construct from temporary std::string
, I made the
choice that it’s consructor does not accept a temporary RAII class, because
that’s the safest option as it’s harder to have dangling reference to a
temporary that’s out of scope, with the minor inconvenience that one can’t do
read(open(...), ...);
on a single line using the file functions below, instead
two lines will be required.
This handle_arg
helps because the options for functions that need a handle
are:
- provide two overloads, one that takes a raw handle and another one that takes a RAII class reference
- call
.get()
each time on the RAII class to pass it to a C++ function that only takes the raw handle
Note that the RAII class raii_with_invalid_value
does not provide an implicit
conversion to the raw handle because that creates the risk that it’s used to
call the function that closes the handle. Similarly std::unique_ptr
does not
convert to the raw pointer, to avoid accidental delete some_unique_ptr;
, one
has to call .get()
instead. Similarly std::string
does not convert to char
*
, in that case to avoid occasional ambiguity with operator[]
, one
has to call .c_str();
instead.
cstdio_lib/file_raii.h
Given raii_with_invalid_value
, we can now easily create file_raii
, a
wrapper for FILE *
. It’s single responsibility is to ensure that a valid
FILE *
is closed using fclose
precisely once. Similarly file_arg
uses
handle_arg
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#pragma once
#include "../cpp_util_lib/raii_with_invalid_value.h"
#include "../cpp_util_lib/handle_arg.h"
#include <cstdio>
namespace cstdio
{
namespace detail
{
struct file_raii_traits
{
using handle = FILE *;
static constexpr auto invalid_value = nullptr;
static void close_handle(handle h) noexcept
{
static_cast<void>(std::fclose(h));
}
};
}
using file_raii = cpp_util::raii_with_invalid_value<detail::file_raii_traits>;
using file_arg = cpp_util::handle_arg<file_raii>;
}
NOTE: for the cases where the invalid value involves a reinterpret_cast
, as
it’s the case in Windows for INVALID_HANDLE_VALUE
, you might need to use
static inline auto invalid_value = ...
instead of the syntax above that uses
constexpr
. Reason is constexpr
does not like reinterpret_cast
.
cstdio_lib/error.h
These are just some functions to handle errors by raising exceptions. It turns
out that there are recurring patterns of error handling. A better
implementation would use std::system_error
instead of std::runtime_error
.
1
2
3
4
5
6
7
8
#pragma once
namespace cstdio::error
{
void throw_errno(const char * function_name, int errno_value);
void throw_errno(const char * function_name);
void throw_failed(const char * function_name);
}
cstdio_lib/error.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "error.h"
#include <cstring>
#include <stdexcept>
#include <string>
namespace cstdio::error
{
void throw_errno(const char * function_name, int errno_value)
{
std::string message = "Function ";
message += function_name;
message += " failed. Error: ";
message += std::strerror(errno_value);
throw std::runtime_error(message);
}
void throw_errno(const char * function_name)
{
throw_errno(function_name, errno);
}
void throw_failed(const char * function_name)
{
std::string message = "Function ";
message += function_name;
message += " failed";
throw std::runtime_error(message);
}
}
cstdio_lib/file.h
Here is the basic API wrapping level. It consists of functions grouped in a namespace. Their single responsibility it to be thin wrappers that deal with error handling.
It is not the traditional OOP approach, which surprises some, but it has it’s advantages.
For input parameters most require a file_arg
which means they can be used
with a file_raii
instance, but also with a non-owning FILE *
like stdout
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma once
#include "file_raii.h"
#include <cstddef>
namespace cstdio::file
{
file_raii open(const char * file_name, const char * mode);
size_t read(file_arg h, char * buffer, size_t size);
void write(file_arg h, const char * buffer, size_t size);
bool is_eof(file_arg h);
void close(file_raii & x);
}
cstdio_lib/file.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include "file.h"
#include "error.h"
namespace cstdio::file
{
file_raii open(const char * file_name, const char * mode)
{
file_raii result{ std::fopen(file_name, mode) };
if (!result.is_valid())
{
error::throw_errno("fopen");
}
return result;
}
size_t read(file_arg h, char * buffer, size_t size)
{
size_t read_count{ std::fread(buffer, 1, size, h) };
if ((read_count != size) && ferror(h))
{
error::throw_errno("fread");
}
return read_count;
}
void write(file_arg h, const char * buffer, size_t size)
{
size_t write_count{ std::fwrite(buffer , 1, size, h) };
if (write_count != size)
{
error::throw_errno("fwrite");
}
}
bool is_eof(file_arg h)
{
return (0 != std::feof(h));
}
void close(file_raii & x)
{
int result = std::fclose(x.release());
if (result != 0)
{
error::throw_failed("fclose");
}
}
}
app_lib/file_util.h
1
2
3
4
5
6
7
8
#pragma once
namespace app_lib::file_util
{
void copy_file(
const char * src_file_name,
const char * dst_file_name);
}
app_lib/file_util.cpp
copy_file
is build on top of the basic API wrapping level. The whole point of
this idiom is to make easy to write functions like this correctly.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "file_util.h"
#include "../cstdio_lib/file.h"
namespace app_lib::file_util
{
void copy_file(
const char * src_file_name,
const char * dst_file_name)
{
auto src = cstdio::file::open(src_file_name, "rb");
auto dst = cstdio::file::open(dst_file_name, "wb");
char buffer[1024];
do
{
size_t read_count = cstdio::file::read(src, buffer, sizeof(buffer));
if (read_count > 0)
{
cstdio::file::write(dst, buffer, read_count);
}
} while ( ! cstdio::file::is_eof(src));
cstdio::file::close(dst);
}
}
cpp_util_lib/console.h
The function here is not strictly necessary for this simple example, but useful if you’re writing more than one executable.
It’s responsibility is to handle exceptions at a boundary (e.g. main in this case) over which exceptions should not be thrown.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma once
#include <iostream>
namespace cpp_util::console
{
template<typename Fn>
int main_wrap(Fn fn)
{
try
{
fn();
std::cout << "\nOK\n";
return 0;
}
catch(const std::exception & e)
{
std::cerr << "\nException: " << e.what() << "\nFAILED\n";
return 1;
}
}
}
app/main.cpp
The single responsibility of main
is to provide the entry point into the
application.
1
2
3
4
5
6
7
8
#include "../cpp_util_lib/console.h"
#include "../app_lib/file_util.h"
int main() {
return cpp_util::console::main_wrap([]() {
app_lib::file_util::copy_file("src.bin", "dst.bin");
});
}
Single responsibility
In this idiom example I tried to show how to code in such a way that each unit (set of .h and .cpp files) has a clear responsibility. That is important for testability, reuse.
Checking return code for fclose
Some functions that free resources cannot fail in a correctly written program,
on correct hardware. E.g. delete
will succeed (assuming that it’s called
correctly, once for memory allocated with new
).
Some functions that free resources return an error code. Of this
category some only have a nominal error code, in practice with correct inputs
they will succeed (they are similar to delete
above).
However fclose
is different. Documentation might not be clear, but often
inside fclose
the file is flushed to the disk, which might fail (especially
if the file is remote).
The strategy here is:
- We explicitly call
close
at the end ofcopy_file
. Iffclose
fails,close
throws andcopy_file
trows as well, indicating failure. - Also
close
sets theFILE *
tonullptr
to ensurefclose
will not be called again in the destructor. To do that, it takes the argument as afile_raii
reference instead of afile_arg
. - The return value of
fclose
infile_raii_traits
is ignored. It is called from destructors and move assignment, that is not the place to throw. Another option would have been to callstd::terminate
, like instd::thread
, but this is probably not appropriate.
Argument validation
One observation with the approach above is that there is no extensive runtime
validation. Interface contracts are sometimes narrow. E.g. the functions in the
cstdio::file
namespace that receive a FILE *
expect it to be valid, they do
not validate it (not even checking for nullptr
, though we could have put some
debug build only asserts).
The point is to address automatically some concerns, not to prevent logical errors at runtime cost.
Summary
At a superficial look this approach looks more verbose than the classic RAII, slim RAII, fat RAII and even pure C approaches. It totals 325 lines of code.
However it will pay off on various scenarios that are likely in a large project where you will write more code that reuses existing code in new combinations.