To RAII or Not to RAII?
RAII is a central concept in C++, that consists in relying on the compiler to call destructors automatically in certain cases. Putting appropriate code in such destructors then relieves us from calling that code – the compiler does it for us.
RAII is an idiomatic technique of C++, but can we use RAII for everything? Is it a good idea to shift every possible piece of code to the destructor of some class, to leave the work to the compiler and make calling code as light as can be?
Since this question comes down to asking if the proverbial hammer is a tool fit for every single task, the answer to that question is probably the proverbial No.
But then, in which cases would RAII improve the design of a piece of code?
In this article we’ll see a case where RAII is adapted, then a case where RAII is NOT adapted. And after that we’ll see a case open to discussion. We’ll then conclude with how to use levels of abstractions to make the decision to RAII or not to RAII.
A typical case for RAII: smart pointers
Smart pointers are classes that contain a pointer and take care of deleting them when going out of scope. If this sentence doesn’t make sense, you can look at this refresher on smart pointers, where we get into more details about the stack, the heap, and the principle of RAII illustrated with smart pointers.
Smart pointers are considered an improvement over raw pointers (the “smart” qualification says something about them). Pointers allow dynamic allocation useful for polymorphism, but are difficult to deal with, particularly with their life cycle. Indeed, if we forget to call delete
on a pointer it causes a memory leak, and if we call delete
more than once we get undefined behaviour, typically a crash of the application.
Moreover, some functions can return earlier than the end of their implementation because of an early return or an uncaught exception. In those cases it is tricky to make sure we call delete
correctly.
Smart pointers relieve us from those troubles (Hey, people from other languages, C++ is getting simpler!), and they do it by using RAII. Indeed, when a smart pointer is instantiated manually on the stack, or returned from a function, or contained in a object, the compiler automatically calls its destructor which in turns calls delete
on the raw pointer. Even in the case of function with an early return or uncaught exception.
(Well, there are various smart pointers, and some of them like std::shared_ptr
have a more elaborate way to deal with memory, but that’s essentially the idea.)
So in the case of smart pointer, using RAII is considered to be a good thing.
A distortion of RAII
EDIT: this section has gone through some changes since the original version of the article, thanks to Andrew Haining and Daryn’s inputs. I’m grateful to them for this.
Just for the sake of illustrating a case where putting a destructor in charge of some logic is not adapted, let’s consider the following slightly contrived example.
We have a Persistor
class in charge of saving some data in a database. It receives this data through its constructor. Now let’s suppose we use something that looks like RAII to trigger the saving procedure of the data, so we put everything related to saving in its destructor:
class Persistor { public: explicit Persistor(Data const& data); ~Persistor() { // open database connection // save data_ in database // close database connection } private: Data data_; };
In this case, a calling code could look like this:
void computeAndSaveData() { Data data = // code that // computes the // data to be saved Persistor myPersistor(data); // we just create a Persistor } // myPersistor's destructor is called - the data gets saved
This code has the problem that it would trigger a question in the mind of its reader: why isn’t this variable used? To this we could answer why else a persistor would be there unless to save data? But still, the code would be clearer if it just mentioned that it did a saving operation.
Let’s move the code saving the data from the destructor over to a new save
method. The calling code is now:
void computeAndSaveData() { Data data = // code that // computes the // data to be saved Persistor myPersistor(data); myPersistor.save(); }
Which is clearer.
However, it would make sense to leave some code to RAII in the destructor: the closing of the database connection for instance. So we’d be using RAII for this, and that would be somewhat similar to smart pointers: we would dispose of a resource in the class destructor.
Here is how the code would look like:
class Persistor { public: explicit Persistor(Data const& data) { connection_ = ...; /* open database connection */ } ~Persistor() { /* close database connection */ } save(Data data) { /* save data in database */ } private: DatabaseConnection connection_; };
At this point it is worthy to note that the ‘R’ in RAII stands for Resource (if you were wondering, the other letters mean “Acquisition Is Inialization”. But I don’t think it matters qs much).
Is this to say that RAII is only useful for making sure we dispose of a resource correctly, and for nothing else?
Let’s see one last example to check that.
A case to discuss: a contextual logger
The case
We have a program that perform many calculations, and we want to log some of these computed values to an output file. Every logged information should be made of two parts:
- the value that the program is computing,
- the context within which this value is computed.
This looks like a project I’ve worked on but I’m stripping off every domain aspect here, so let’s consider an imaginary context of retail.
We have a supermarket that contains departments, that themselves contain items. To compute the total value of the assets owned by a store, we sum all the values of the items contained in each department.
The call stack looks like this one:
Now here is an excerpt of the desired output log:
Store = Madison Av > Dpt = Laptops > Item #42 | Item value = 1000 Store = Madison Av > Dpt = Laptops > Item #43 | Item value = 500 Store = Madison Av > Dpt = Laptops | Item value = 1500 Store = Madison Av > Dpt = Webcams > Item #136 | Item value = 12
Each message starts with a context that can have various depths corresponding the levels of the call stack, and ends with a message about a local calculation.
The first two lines and the 4th one are emitted from the computeItemValue
function. They output the value of the current item being considered. The third line is emitted from the computeDepartmentAssets
function, that adds up the values of the items in a department.
How can we implement such a logger?
A solution using RAII
One way to go about that is to maintain a context stack in the logger, to which we push context information (e.g. Dpt = Laptops
) when the execution enters a given context, and pop it off when it gets out of that context. We can then pile up deeper inner contexts (Item #42
) before going out of an outer context.
To model this, let’s create a Logger
class to which we can push or pop additional context. Logger
also has a log
method that takes a message (the second part of the line) and sends a line constituted of the current context and the message, to the output file:
class Logger { public: pushContext(std::string const& context); popContext(); sendMessage(std::string const& message); };
To push or pop a context, we can use RAII through a helper class Context
. This class accepts an incremental context and pushes to the Logger
in its constructor, and pops it off in its destructor:
class LoggingContext { public: LoggingContext(Logger& logger, std::string const& context) : logger_(logger) { logger_.pushContext(context); } ~LoggingContext() { logger_.popContext(); } private: Logger& logger_; };
We can instantiate this class at the beginning of the function, and allowing to maintain the correct context.
Here is how the call computing the value of an item would perform it logging:
double computeItemValue(Item const& item) { LoggingContext loggingContext(logger, "Item #" + std::to_string(item.getId())); // code that computes the value of an item... logger.sendMessage("Item value = " + std::to_string(value)); // return that value }
And at department level:
double computeDepartmentAssets(Department const& department) { LoggingContext loggingContext(logger, "Dpt = " + department.getName()); // code that calls 'computeItemValue' // and adds up the values of each item logger.sendMessage("Dpt value = " + std::to_string(sum)); // return that sum }
And we’d have something similar at store level.
The variable loggingContext
is instantiated, but not used directly in the function. Its purpose it to push an additional context information to the logger at the beginning of the function, and to pop it when its destructor is called when the function ends.
We use RAII here to pop off the context without having to write code for it, but there is no resource handled here. Is this good design?
Let’s see the advantages and drawbacks of this technique:
Advantages:
- The context is popped off the logger no matter how the function ends (normal ending, early return, uncaught exception)
- A declarative approach: the calling code merely states that it is about a given context, and doesn’t have to manipulate the logger.
- This has a side effect to document the code for readers too, to say what a piece of code is about (we’ve used it for a whole function, but this technique can also be used in a block inside a function (delimited by braces
{}
))
Drawbacks:
- An unused variable can be surprising.
What is your opinion on this?
There is one important aspect here: some code (manipulating the logger) has been hidden from the calling context. Is it a good thing or a bad thing? It comes down to…
Levels of abstraction
The code that computes the assets of a department in a store, for example, shouldn’t be too much concerned with logging. Its main job is to perform calculations, and the fact that it sends them to a log is incidental. And how exactly the logger works, with its contexts stacking up and everything, is not part of the abstraction of a function that performs calculations.
Here RAII encapsulates this lower level of abstraction of how we do logging and lets the function express what it is about, by stating its own context. RAII helped us respecting levels of abstraction here.
Let’s try to see the previous examples, the smart pointer and the database saving, with the perspective of levels of abstraction.
Smart pointers use RAII to hide the manipulation of pointers, which are a lower level than business code, so RAII helps respect levels of abstraction in that case too. This is true for resource management in general, including database connection. We just want resources to be managed correctly, and not to pollute our higher-level business code.
The code that saves the data in the Persistor
example is at the level of abstraction of the code that instantiates the persistor. Indeed, the role of the calling function was to save, and RAII got in the way by taking this code away to a destructor, so it wasn’t adapted to that situation.
Two aspects of RAII
In conclusion, we’ve seen two aspects to consider to decide whether or not to use RAII in a given situation:
- is there code we want to be called no matter how a function ends, be there normal ending, early return or uncaught exceptions (which is the case for releasing a resource and popping off the logger)?
- does it help respect levels of abstraction?
If one of those two questions answers Yes, RAII is an option you want to give a chance to.
Don't want to miss out ? Follow:   Share this post!