Classic RAII
RAII (Resource Acquisition Is Initialization) is a difficult to say name that does not do justice to one of the most useful programming idioms. This article describes the classic way of using RAII, with the full example of the copy file example rewritten.
Introduction
RAII is a resource management technique developed in C++ by Bjarne Stroustrup and Andrew Koenig in the 1980s. It largely eliminates the need to collect garbage, by not creating garbage in the first place and providing ease of use and correctness when dealing with resources (e.g. memory, files, etc.).
In this article I’ll describe what I call the classical RAII. As we’ll see later, there are some minor variations.
RAII fundamentally works by wrapping a resource in an object, initializing the resource in the constructor. If the constructor succeeds, the object can be used and the destructor will automatically cleanup the resource. If the constructor fails, it throws an exception, the object can’t be used and conveniently the destructor is not called.
When opening a file with fopen
, the resource that needs to be wrapped is a
FILE *
. In the constructor we need to open the file. If opening the file
succeeds, the destructor will call fclose
. If opening a file fails (e.g.
because of the file permissions) the constructor throws an exception and the
destructor is not called.
A class to wrap the FILE *
using a classical RAII idiom would look like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class file
{
FILE * f_;
public:
file(const char * file_name, const char * mode)
{
f_ = fopen(file_name, mode);
if ( ! f_)
{
perror(0);
throw std::runtime_error("Failed to open file");
}
}
~file()
{
fclose(f_);
}
// methods
// that use f_
// assume f_ is not null
};
Notice that the construction has two possible outcomes: the constructor succeeds or it throws.
If fopen
succeeds, then the constructor succeeds and the instance has a f_
that is not null. f_
can be used in other file
class methods without the
need to test for null, including in the destructor. This behaviour explains the
complicated RAII name (Resource Acquisition Is Initialization): when the
resource is acquired, the object is fully initialized.
If fopen
fails, the constructor throws: the object is not constructed, the
user can’t call methods and the destructor is not called.
Advantages
First of all notice how fclose
can be arranged to be placed close to the
matching fopen
(locality), they are no longer getting further away as
non-related code gets added.
Secondly notice how the file
class encapsulates the logic of managing the
FILE *
. To open and use two files, one needs to construct them like this:
1
2
3
4
5
file src{ "src.bin", "rb" };
file dst{ "dst.bin", "wb" };
// more
// code
// here
On the happy path this will create two file
instances, each in charge with
its own FILE *
. When they go out of scope they will each be destructed,
closing each file, in the reverse order of the declaration: src
is created
first, and destructed last. The scope of dst
is surrounded by the scope of
src
.
If say the creation of dst
fails, then the execution exits the scope,
ensuring that the destructor if src
is called closing its already opened
FILE *
. This provides exception safety for the resources on the stack.
Issues
No real issues, more like things to pay attention to:
- Ensure the destructors don’t throw
- Pay attention to the copy constructor and assignment operator. One easy option is to delete them to ensure that destructor does not try to release twice the same resource.
Note: Strictly speaking the std::vector would initialize to 0
which is
additional work comparing with just allocating memory like the C example.
Full code
file.h
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 <cstdio>
class file
{
FILE * f_;
public:
file(const char * file_name, const char * mode);
// used to be "private:" before "= delete"
file(const file &) = delete;
file & operator=(const file &) = delete;
~file();
size_t read(char * buffer, size_t size);
void write(const char * buffer, size_t size);
bool is_eof();
private:
void log_and_throw(const char * message);
};
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
#include "file.h"
#include <stdexcept>
file::file(const char * file_name, const char * mode)
{
f_ = fopen(file_name, mode);
if ( ! f_)
{
log_and_throw("Failed to open file");
}
}
file::~file()
{
fclose(f_);
}
size_t file::read(char * buffer, size_t size)
{
size_t read_count = fread(buffer, 1, size, f_);
if ((read_count != size) && ferror(f_))
{
log_and_throw("Failed to read from file");
}
return read_count;
}
void file::write(const char * buffer, size_t size)
{
size_t write_count = fwrite(buffer, 1, size, f_);
if (write_count != size)
{
log_and_throw("Failed to write to file");
}
}
bool file::is_eof()
{
return feof(f_);
}
void file::log_and_throw(const char * message)
{
perror(0);
throw std::runtime_error(message);
}
application.h
1
2
3
#pragma once
void copy_file();
application.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "application.h"
#include "file.h"
#include <iostream>
#include <vector>
void copy_file()
{
file src{ "src.bin", "rb" };
file dst{ "dst.bin", "wb" };
constexpr size_t buffer_size{ 1024 };
std::vector<char> buffer(buffer_size);
do
{
size_t read_count = src.read(buffer.data(), buffer_size);
if (read_count > 0)
{
dst.write(buffer.data(), read_count);
std::cout << '.';
}
} while ( ! src.is_eof());
}
main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "application.h"
#include <iostream>
int main ()
{
try
{
copy_file();
std::cout << "\nSUCCESS\n";
return 0;
}
catch(const std::exception & e)
{
std::cerr << e.what() << std::endl;
return 1;
}
}
Code discussion
We now have five source files, with a total of 114 lines of code. The code to
actually copy the file (in copy_file
in application.cpp
) takes 15 lines
of code however, which is shorter even than the 20 lines of code in the
version without any error checking mainly because the file
class
takes care of the now trivial issues of checking for errors and releasing
resources.
I believe this is one aspect of what Bjarne Stroustrup means when he
says that “in realistic systems, there are far more
resource acquisitions than kinds of resources, so the RAII technique leads to
less code”. In this example the file
class is used only twice but this is
compensated by the fact that library classes are used a lot (e.g. think
std::vector
).
The other thing to note is that we separated the code in several parts that each uses a subset of the C++ language:
file.h
andfile.cpp
implement RAII. They care about construction, destruction, copying, checking for API errors.application.h
andapplication.cpp
are the core functionality. They assume the RAII classes are correct and that somewhere the exceptions are caught, but the code has a very different feel.- and finally
main.cpp
just cares about error handling and invoking the application function.
Summary
RAII coding style avoids repetition and reduces errors through encapsulation and locality of resource management. It is a very important programming idiom, with a difficult name unfortunately. Usage of some RAII variant is highly recommended.