Open In App

Resource Acquisition Is Initialization

Last Updated : 08 Nov, 2023
Improve
Improve
Like Article
Like
Save
Share
Report

RAII stands for “Resource Acquisition Is Initialization”. Suppose there is a  “resource” in terms of Files, Memory, Sockets, etc. RAII means that an object’s creation and destruction are tied to a resource being acquired and released.

Let’s assume we have to write events to a log file. A non-object-oriented way to do this would be:

C++




void WriteToLog(int32_t log_fd, const std::string& event)
{
    // use 'write' syscall to write to the log.
}
 
int main(int argc, char** argv)
{
    int32_t log_fd = open("/tmp/log", O_RDWR);
 
    if (log_fd < 0) {
        std::cerr << "Failed to open log file";
        return -1;
    }
 
    WriteToLog("Event1");
    WriteToLog("Event2");
 
    // What are we missing here ?
    return 0;
}


We forgot to close log_fd. It’s imperative that every Linux process closes its file descriptors during its lifetime; otherwise, it may run out of file descriptors and misbehave/fail. Additionally, leaving file descriptors open will cause the kernel to keep extra bookkeeping and state around for unused yet open file descriptors and their backing file objects, resulting in increased memory consumption and unnecessary work.

An Object-Oriented Approach

Consider a Logger class that writes events to a log file. In order to write to the file, it will need to open a file descriptor to it. Let’s take a look at how a primitive version of this class might look:

C++




class Logger {
public:
    Logger();
 
    bool Initialize(const std::string& log_file_path)
    {
        log_fd_ = open(log_file_path, O_RDWR);
        return (log_fd_ < 0 ? false : true);
    }
 
    void Log(const std::string& event);
 
    ~Logger()
    {
        // Clean up our resources before the object goes
        // away.
        close(log_fd_);
    }
 
private:
    int32_t log_fd_;
}


Now let’s see how we will use this API:

C++




int main(int argc, char** argv)
{
    Logger logger = Logger();
    if (!logger.Initialize("/tmp/log")) {
        std::cerr << "Failed to initialize logger";
        return -1;
    }
    logger.Log("some random event");
    return 0;
}


The good thing is that upon destruction, the `Logger` closes the underlying file descriptor, making it responsible for its own resources.

However, the `logger` object is in limbo between the `Constructor` and the `Initialize` call, as it has no file to write to internally. Relying on “good and responsible” users to use our API judiciously is always a bad code smell.

Let’s see if we can do better:

RAII To The Rescue

Putting RAII into practice, let’s acquire the file in the `Logger` constructor itself. Here’s how it would look:

C++




class Logger {
public:
    explicit Logger(const std::string& log_file_path)
    {
        log_fd_ = open(log_file_path, O_RDWR);
    }
 
    void Log(const std::string& event)
    {
        // Write the event minus the null terminator.
  if ((write(log_fd_, event.c_str(), event.length()) != event.length) {
            std::cerr << "Failed to log event: " << event;
            return;
     }
    }
 
    ~Logger()
    {
        // Clean up our resources before the object goes
        // away.
        close(log_fd_);
    }
 
private:
    int32_t log_fd_;
}


 

This version improves our previous API by acquiring the file in the constructor. It still has one flaw but let’s see how we use this API:

C++




int main(int argc, char** argv)
{
    Logger logger = Logger("/tmp/log");
 
    // What happens here if log_fd_ < 0 in the Constructor.
    logger.Log("some random event");
 
    return 0;
}


If we fail to open the log file, then all logging events will fail. We could have `Log` return an error, but the client may keep retrying. To the client, the object creation succeeded, implying that the `Logger` is ready to log events. Any `Log` errors may be transient and worth retrying.

However, this is not the case. All `Log` calls will fail. We should have returned an error where it happened – in the constructor. Constructors, unfortunately, cannot return errors. Let’s see an elegant way to get around this.

RAII With Acquisition Error Handling

Let’s return a `std::unique_ptr` from a static constructor. Internally, this will create a `Logger` object on the heap, wrap it in a `unique_ptr`, and return it to the client. If we fail to open the log file, we will return a `nullptr` instead. This way, we will inform the client that we failed when we actually failed.

Let’s see how we can use this API:

C++




class Logger {
public:
    static std::unique_ptr<Logger>
    Create(const std::string& log_file_path)
    {
        int32_t log_fd = open(log_file_path, O_RDWR);
        if log_fd
            < 0 { return nullptr; }
 
        return std::make_unique<Logger>(log_fd);
    }
 
    void Log(const std::string& event)
    {
        // Write the event minus the null terminator.
  if ((write(log_fd_, event.c_str(), event.length()) != event.length) {
            std::cerr << "Failed to log event: " << event;
            return;
     }
    }
 
    ~Logger()
    {
        // Clean up our resources before the object goes
        // away.
        close(log_fd_);
    }
 
private:
    explict Logger(int32_t log_fd)
        : log_fd_(log_fd)
    {
    }
    int32_t log_fd_;
}


Now that the client knows when a `Logger` creation fails, it can take necessary steps without being confused by `Log` API failures. When `logger` goes out of scope, its destructor is called, and the log file is closed.

Other Scenarios where RAII can be used

1. To automatically join a thread:

When we create a thread in C++, we have to either join() or detach() the thread. In case some exception occurs in the function, and we are not able to join() or detach() the thread, the thread will become a zombie thread. To avoid this, we can use RAII:

C++




#include <iostream>
#include <thread>
 
void func2()
{
    std::cout << "We are in func2\n";
}
 
void func1()
{
    std::cout << "We are in func1\n";
    std::thread t1(func2);
    std::exception e;
 
    try
    {
      //Does some processing
        for (int i = 0; i < 100; i++)
        {
            //Lets say an exception is thrown at some condition
            if (i == 60)
                throw e;
        }
    }
    catch (...)
    {
        throw;
    }
 
    t1.join();
}
 
int main() {
    try {
        func1();
    }
    catch (...){
    }
    return 0;
}


Output: Exception Unhandled.

In this program, we are calling func1() inside a try-catch block in main(). If func1() throws an exception, we’ll catch the exception in main().

Now, func1() is called, and it creates a thread t1. Then it does some processing and expects to join t1 after the processing is done. But unfortunately, some exception occurs during this process, and func1 rethrows the exception in the catch block, without joining t1. He hopes to catch the exception in main(), but the program will throw an error because t1 has not been joined.

In a deeper calling stack, we can get trapped in this error quite easily. One way to solve this is to create a Wrapper that will join the thread automatically when the thread goes out of scope.

RAII Thread Wrapper

C++




#include <iostream>
#include <thread>
 
class Wrapper
{
private:
    std::thread *t;
public:
    Wrapper(std::thread *thread)
    {
        t = thread;
    }
    ~Wrapper()
    {
        t->join();
    }
};
 
void func2()
{
    std::cout << "We are in func2\n";
}
 
void func1()
{
    std::cout << "We are in func1\n";
    std::thread t1(func2);
    Wrapper w(&t);
    std::exception e;
 
    try
    {
        for (int i = 0; i < 100; i++)
        {
            //Lets say an exception is thrown at some condition
            if (i == 60)
                throw e;
        }
    }
    catch (...)
    {
        throw;
    }
 
//    t1.join();
}
 
int main() {
    try {
        func1();
    }
    catch (...) {
    }
    return 0;
}


Output: We are in func1
We are in func2

Here we are creating an object of Wrapper i.e. w. In the constructor, we are passing the pointer to thread t1. The constructor assigns its pointer member variable std::thread *t with the pointer of thread t1. When the exception is thrown and func1() ends, the destructor of the Wrapper Class is invoked and it joins t1 automatically.

2. Scoped Pointers:

Scoped pointers are pointers that point to a heap-allocated memory and are deleted when the scope in which they are defined ends. Memory leak occurs when we forget to delete pointers that point to heap-allocated memory. This can exhaust our heap. So, it is very important to delete and free the heap memory we are not going to use.

The question is that if we want to delete the memory when the scope ends, why not use the stack memory? Obviously, we should always use stack memory whenever we can because it is faster to initialize and easier to manage. We only use heap memory when we need the memory to be shared between different scopes. So, what is the use case of scoped pointers?

When you need to allocate a large amount of memory, The stack might not be helpful because stack size is limited. If allocate more memory in the stack, stack overflow will occur. So to allocate large amounts of memory, we use heap allocation.

Scoped Pointer:

C++




#include<iostream>
#include<string>
class Scoped_ptr {
private:
    void* ptr;
    std::string type;
 
public:
    Scoped_ptr(void* p, const char* type)
    {
        std::cout << "Constructor: Creating " << type << std::endl;
        ptr = p;
        this->type = type;
    }
    ~Scoped_ptr()
    {
        if (type == "int")
        {
            std::cout << "Destroying " << type << ", Value: " << *(int*)ptr<<std::endl;
            delete (int*)ptr;
        }
        else if (type == "string")
        {
            std::cout << "Destroying " << type << ", Value: " << *(std::string*)ptr << std::endl;
            delete (std::string*)ptr;
        }
        else if (type == "float")
        {
            std::cout << "Destroying " << type << ", Value: " << *(float*)ptr << std::endl;
            delete (float*)ptr;
        }
        else if (type == "double")
        {
            std::cout << "Destroying " << type << ", Value: " << *(float*)ptr << std::endl;
            delete (double*)ptr;
        }
        else
        {
            //Other types.
        }
    }
 
 
};
 
int main()
{
    Scoped_ptr s1((void*)(new int(5)), "int");
    Scoped_ptr s2((void*)(new std::string("Hello")), "string");
}


Output: Constructor: Creating int
Constructor: Creating string
Destroying string, Value: Hello
Destroying int, Value: 5

In this program, we are creating a class Scoped_ptr and passing a void pointer to it, so that we can support all types. Since we need to know the kind of pointer when we delete it, we also pass a parameter ‘type’ and cast our pointer to that type. When the scope ends, the heap-allocated object gets deleted automatically.

This implementation is just an example of how Scoped Pointers can be implemented. The real implementation of Scoped Pointer is based on templates. You can learn templates and implement Scoped pointers using them as an exercise.

Conclusion

In supported languages, RAII is a powerful tool to abstract resources without manually managing them. It prevents leaks and nasty bugs. Developers should use it to make their life easier.



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads