Slim RAII
This article looks at handling errors in C APIs using an if-error-return coding pattern which uses a slimmed down RAII variant which is sometimes useful.
Introduction
I mentioned that there are variants of RAII, and this describes the slimmed down variant that uses very simple classes to wrap resources and ensure they are released.
This approach is useful for example when working with an unfamiliar C API. Say
at the stage when you’re not sure: should fread
belong to the file
class or
to the buffer
class? This is a serious question, because there is a ‘smell’
in code that if a function member of a class needs lots of member variables
from another class, maybe it belongs to the wrong class. fread
needs both the
pointer to and the size of data from the buffer
, and only the file pointer
from the file
.
In this slimmed down RAII variant we have a simple class that gets the resource
on the constructor and frees it in the destructor. It is so slimmed down that
the resource is accessible as a public member variable, and there no other
member functions. To even remove the need to declare public
accessibility the
class is actually a struct
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct file
{
FILE * p;
explicit file(FILE * x) noexcept :
p{ x }
{
}
~file()
{
if (p)
{
fclose(p);
}
}
};
To initialize and use this class one needs to use the if-error-return pattern:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
file src{ fopen("src.bin", "rb") };
if ( ! src.p)
{
perror("Failed to open source file");
return 1;
}
// more
// code
// here
size_t read_count = fread(buffer.p, 1, buffer_size, src.p);
if ((read_count != buffer_size) && ferror(src.p))
{
perror("Failed to read from source file");
return 1;
}
Another situation where this approach might be useful is where you need more control over if and when exceptions are thrown.
Issues
The issue with this approach is the repetition, that it shares with all the
if-error patterns. While resources are released by the
destructor, the repetition for the if
blocks is still a major issue. Every
time we call some function we need to repeat:
- the if condition
- the error handling
- the return statement
- and 2 curly brackets
Full code
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
#include <stdio.h>
#include <stdlib.h>
struct file
{
FILE * p;
explicit file(FILE * x) noexcept :
p{ x }
{
}
~file()
{
if (p)
{
fclose(p);
}
}
file(const file &) = delete;
file & operator=(const file &) = delete;
};
struct mem_buffer
{
char * p;
explicit mem_buffer(char * x) noexcept :
p{ x }
{
}
~mem_buffer()
{
if (p)
{
free(p);
}
}
mem_buffer(const mem_buffer &) = delete;
mem_buffer & operator=(const mem_buffer &) = delete;
};
int main ()
{
file src{ fopen("src.bin", "rb") };
if ( ! src.p)
{
perror("Failed to open source file");
return 1;
}
file dst{ fopen("dst.bin", "wb") };
if ( ! dst.p)
{
perror("Failed to open destination file");
return 1;
}
constexpr size_t buffer_size{ 1024 };
mem_buffer buffer{ reinterpret_cast<char *>(malloc(buffer_size)) };
if ( ! buffer.p)
{
fputs("Failed to allocate buffer\n", stderr);
return 1;
}
do
{
size_t read_count = fread(buffer.p, 1, buffer_size, src.p);
if ((read_count != buffer_size) && ferror(src.p))
{
perror("Failed to read from source file");
return 1;
}
if (read_count > 0)
{
size_t write_count = fwrite(buffer.p, 1, read_count, dst.p);
if (write_count != read_count)
{
perror("Failed to write to destination file");
return 1;
}
fputs(".", stdout);
}
} while ( ! feof(src.p));
fputs("\nSUCCESS\n", stdout);
return 0;
}
Summary
Use it as a stopgap measure to ensure resources are released while learning a C API or in a very localized area handling a C API.