Fit RAII
This article looks at an idiom of wrapping C APIs that increases code reuse compared with the classic, slim and fat RAII variants described in previous articles.
NOTE: for an updated version of this article see fit RAII - revisited.
Introduction
In previous articles I’ve described several idioms of wrapping C APIs such as classic RAII, slim RAII and fat RAII.
While they work, there are alternatives that scale better in larger projects. This article tries to sketch one such alternative. I’ll call it ‘fit RAII’.
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 *
. 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 free 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
#pragma once
namespace cpp_util
{
template<typename Traits>
// requires Traits defines
// - a Handle type - the type of the handle
// - a function static void free_handle(Handle) noexcept - the function to
// close the handle
// - and a invalid_value constant - the special value indicating no handle is
// hold
class raii_with_invalid_value
{
using Handle = typename Traits::Handle;
static constexpr auto invalid_value = Traits::invalid_value;
Handle h_;
public:
raii_with_invalid_value() noexcept :
h_{ invalid_value }
{
}
explicit raii_with_invalid_value(Handle h) noexcept :
h_{ h }
{
}
~raii_with_invalid_value()
{
if (is_valid())
{
Traits::free_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_ = invalid_value;
}
raii_with_invalid_value & operator=(raii_with_invalid_value && other) noexcept
{
Handle tmp = other.h_;
other.h_ = invalid_value;
if (is_valid())
{
Traits::free_handle(h_);
}
h_ = tmp;
return *this;
}
bool is_valid() noexcept
{
return h_ != invalid_value;
}
Handle get() noexcept
{
return h_;
}
Handle & handle_reference() noexcept
{
return h_;
}
Handle * handle_pointer() noexcept
{
return &h_;
}
void reset(Handle h = invalid_value) noexcept
{
if (is_valid())
{
Traits::free_handle(h_);
}
h_ = h;
}
Handle release() noexcept
{
Handle tmp = h_;
h_ = invalid_value;
return tmp;
}
};
}
The single purpose of the class is to free the resource on destructor, if it does not have the invalid value.
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. Copying a file handle is either not possible or possible but undesirable most of the time.
The class is moveable and has a default constructor which makes it useful in a
variety of scenarios not illustrated in the code below. E.g. when it needs to be
default created and the handle is provided later even indirectly via the
handle_pointer
method.
The get
, reset
and release
methods create some similarities in usage to
smart pointers.
cstdio_lib/file_raii.h
Given raii_with_invalid_value
, we can now easily create a wrapper for FILE
*
. It’s single responsibility is to ensure that a valid FILE *
is closed
using fclose
precisely once.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma once
#include "../cpp_util_lib/raii_with_invalid_value.h"
#include <cstdio>
namespace cstdio
{
namespace detail
{
struct file_raii_traits
{
using Handle = FILE *;
static constexpr auto invalid_value = nullptr;
static void free_handle(Handle h) noexcept
{
static_cast<void>(fclose(h));
}
};
}
using file_raii = cpp_util::raii_with_invalid_value<detail::file_raii_traits>;
}
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.
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.
For input parameters they still require a FILE *
which means they can be used
with stdout
as well.
It is not the traditional OOP approach, which surprises some, but it has it’s advantages.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma once
#include "file_raii.h"
#include <cstdio>
#include <cstddef>
namespace cstdio::file
{
file_raii open(const char * file_name, const char * mode);
size_t read(FILE * h, char * buffer, size_t size);
void write(FILE * h, const char * buffer, size_t size);
bool is_eof(FILE * 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 * 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 * 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 * 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.get(), buffer, sizeof(buffer));
if (read_count > 0)
{
cstdio::file::write(dst.get(), buffer, read_count);
}
} while ( ! cstdio::file::is_eof(src.get()));
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. - 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 287 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.