I've read a random quote online about "RAII in C++ is only possible with exceptions" once too much. I can't take it any more.
TL; DR: this post is not about whether exceptions are good or bad. What it is about is RAII as a C++ dynamic resource management technique that stands on its own and is useful with or without exceptions. In particular, I want to explain why RAII is indeed useful even if you have exceptions disabled in your C++ code.
The basics
Let's take the poster child of RAII, an auto-closing handle to wrap FILE* [1]:
class FileHandle {
public:
FileHandle(const char* name, const char* mode) {
f_ = fopen(name, mode);
}
FILE* file() {
return f_;
}
~FileHandle() {
if (f_ != nullptr) {
fclose(f_);
}
}
private:
FILE* f_;
};
Here's an example of how we'd use it:
std::string do_stuff_with_file(std::string filename) {
FileHandle handle(filename.c_str(), "r");
int firstchar = fgetc(handle.file());
if (firstchar != '$') {
return "bad bad bad";
}
return std::string(1, firstchar);
}
Remember: no exceptions here - the code is built with -fno-exceptions and there are no try statements. However, the RAII-ness of FileHandle is still important because do_stuff_with_file has two exit points, and the file has to be closed in each one. do_stuff_with_file is a short and simple function. In a larger function with multiple exit points managing resource release becomes even more error prone, and RAII techniques are paramount.
The essence of RAII is to acquire some resource in the constructor of a stack-allocated object, and release it in the destructor. The compiler guarantees that the destructors of all stack-allocated objects will be called in the right order when these objects go out of scope, whether due to raised exceptions or just because the function returns.
RAII doesn't mean you have to allocate or actually create anything in a constructor. It can do any operation that has a logical "undo" that must be performed later on. A good example is reference counting. Many databases and similar software libraries have abstractions of "cursors" that provide access to data. Here's how we could increase and decrease the reference count on a given cursor safely while working with it:
class CursorGuard {
public:
CursorGuard(Cursor* cursor) : cursor_(cursor) {
cursor_->incref();
}
Cursor* cursor() {
return cursor_;
}
~CursorGuard() {
cursor_->decref();
}
private:
Cursor* cursor_;
};
void work_with_cursor(Cursor* cursor) {
CursorGuard cursor_guard(cursor);
if (cursor_guard.cursor()->do_stuff()) {
// ... do something
return;
}
// ... do something else
return;
}
Once again, usage of RAII here ensures that under no circumstances work_with_cursor will leak a cursor reference: once incref'd, it is guaranteed to be decref's no matter how the function ends up returning.
RAII in the standard library
Such "guard" RAII classes are extremely useful and widespread, even in the standard library. The C++11 threading library has lock_guard for mutexes, for example:
void safe_data_munge(std::mutex& shared_mutex, Data* shared_data) {
std::lock_guard<std::mutex> lock(shared_mutex);
shared_data->munge();
if (...) {
shared_data();
return;
}
shared_data->munge_less();
return;
}
std::lock_guard locks the mutex in its constructor, and unlocks it in its destructor, ensuring that access to the shared data is protected throughout safe_data_munge and the actual unlocking always happens.
RAII and C++11
While on the topic of the standard library, I can't fail mentioning the most important RAII object of them all - std::unique_ptr. Resource management in C and C++ is a big and complex subject; the most common kind of resource managed in C++ code is heap memory. Prior to C++11, there were many third party solutions for "smart pointers", and C++11's move semantics finally allowed the language to have a very robust smart pointer for RAII:
void using_big_data() {
std::unique_ptr<VeryVeryBigData> data(new VeryVeryBigData);
data->do_stuff();
if (data->do_other_stuff(42)) {
return;
}
data->do_stuff();
return;
}
Whatever we do with data, and no matter where how function returns, the allocated memory will be released. If your compiler supports C++14, the line that creates the pointer can be made more succinct with std::make_unique:
// Good usage of 'auto': removes the need to repeat a (potentially long)
// type name, and the actual type assigned to 'data' is trivially obvious.
auto data = std::make_unique<VeryVeryBigData>();
std::unique_ptr is versatile and has other uses, though here I'm just focusing on its value as a RAII enabler for heap memory.
To to stress how important C++11 is for proper RAII: prior to C++11, without move semantics, the only "smart" pointers we could write were really somewhat dumb because they led to too much copying and overhead. There was simply no way to "transfer ownership" of an object from one function to another without considerable overhead. Since C++ programmers are often all about squeezing the last bit of performance from their code, many preferred to just live on the edge and deal with raw pointers. With C++11 and std::unique_ptr, which can be efficiently moved and occupies no additional memory, this problem is much less serious and safety doesn't have to come at the price of performance.
RAII in other languages
A common question asked about C++ is "why doesn't C++ have the finally construct enjoyed by other languages like Java, C# and Python?". The answer, given by Stroustrup himself is that RAII is a replacement. Stroustrup reasons (rightly, IMHO) that in realistic codebases there are far more resource acquisitions and releases than distinct "kinds" of resources, so RAII leads to less code. Besides, it's less error prone since you code the RAII wrapper once and don't have to remember to release the resource manually. Here's the work_with_cursor sample from above rewritten with a hypothetical finally construct:
// Warning: this is not real C++
void work_with_cursor(Cursor* cursor) {
try {
cursor->incref();
if (cursor->do_stuff()) {
// ... do something
return;
}
// ... do something else
return;
}
finally {
cursor->decref();
}
}
Yes, it's a bit more code. But the bigger problem is remembering to call cursor-decref(). Since large codebases juggle resources all the time, in practice you'll end up with try...finally blocks around every function's body and having to remember which resources to release. With our CursorGuard helper, all of that is saved at the cost of a one-time definition of the guard class itself.
A good example to mention here is Python. Even though Python has a finally construct, in modern Python code the alternative with statement is much more widely used. with supports "context managers", which are very similar to C++ RAII. with statements end up being more versatile and nice to use than finally, which is why you'll see more of them in idiomatic code.
So what about exceptions?
I hope that this post has, so far, convinced you that the RAII technique in C++ is important and useful even when exceptions are disabled. The close association people have between RAII and exceptions is warranted, however, because writing exception-safe code without RAII is nearly impossible. With exceptions enabled, we don't just have to examine each explicit return statement in a function to figure out where resources can be leaked. Every line becomes a suspect. Function or method call? Can throw. Creating a new non-POD object on the stack? Can throw. Copying one object to another? Yep, can throw. a + b? Can throw in the + operator.
Another strong link between exceptions and RAII is in constructors. Constructors cannot have return values. Therefore, if a constructor encounters an error condition, you either throw an exception or mark some internal error state. The latter has its issues (which is why alternative methods of construction are recommended in code without exceptions), so throwing an exception is the most common approach. Since RAII is so important for exceptions, and also because RAII and constructors go hand in hand (remember - RAII starts when an object is constructed), the link is burned deep into the minds of C++ students.
But RAII is not just about exceptions. It is about disciplined resource management in C++. Therefore, it makes no sense to assume that RAII somehow means your code is an exception-riddled mess. Or even that it uses exceptions at all. Attacking C++ for its exception safety woes is legitimate, but attacking RAII is less so because RAII is just a solution, it's not the source of the problem.
Finally, on a more personal note, I'll add that while I'm not a big fan of exceptions in C++, I am a huge fan of RAII. When I write C++ code these days, I would rather not use exceptions at all, or at least confine and constrain them to tiny areas in the program. But I use RAII all the time, whether in standard library classes like std::unique_ptr or in my own code. In my mind it's one of the best and most useful features of C++ to help keeping large code bases sane and safe.
[1] | I'm not handling the error condition here. What if fopen failed? Since this post is specifically about exception-less code, throwing an exception is not an option. So some sort of error state is needed to be flagged and checked. There are multiple solutions to this issue, and I'll leave them to a separate post. By the way, a point for consideration: is a "file not found" condition truly horrific enough to warrant an exception? This is a deep question that deals with the very nature of what exceptions should and should not be used for. |