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.



Resource Acquisition Is Initialization

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.

Similar Reads

An Object-Oriented Approach

...

RAII To The Rescue

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:...

RAII With Acquisition Error Handling

...

Other Scenarios where RAII can be used

...