C++ Programming/RAII

From Wikibooks, open books for an open world
Jump to: navigation, search

Resource Acquisition Is Initialization (RAII)[edit]

The RAII technique is often used for controlling thread locks in multi-threaded applications. Another typical example of RAII is file operations, e.g. the C++ standard library's file-streams. An input file stream is opened in the object's constructor, and it is closed upon destruction of the object. Since C++ allows objects to be allocated on the stack, C++'s scoping mechanism can be used to control file access.

With RAII we can use, for instance, Class destructors to guarantee clean up, similar to the finally keyword in other languages. Doing this automates the task and so avoids errors but gives the freedom not to use it.

RAII is also used (as shown in the example below) to ensure exception safety. RAII makes it possible to avoid resource leaks without extensive use of try/catch blocks and is widely used in the software industry.

The ownership of dynamically allocated memory (memory allocated with new) can be controlled with RAII. For this purpose, the C++ Standard Library defines auto ptr. Furthermore, lifetime of shared objects can be managed by a smart pointer with shared-ownership semantics such as boost::shared_ptr defined in C++ by the Boost library or policy based Loki::SmartPtr from Loki library.

The following RAII class is a lightweight wrapper to the C standard library file system calls.

 #include <cstdio>
 
 // exceptions
 class file_error { } ;
 class open_error : public file_error { } ;
 class close_error : public file_error { } ;
 class write_error : public file_error { } ;
 
 class file
 {
 public:
     file( const char* filename )
         :
         m_file_handle(std::fopen(filename, "w+"))
     {
         if( m_file_handle == NULL )
         {
             throw open_error() ;
         }
     }
 
     ~file()
     {
         std::fclose(m_file_handle) ;
     }
 
     void write( const char* str )
     {
         if( std::fputs(str, m_file_handle) == EOF )
         {
             throw write_error() ;
         }
     }
 
     void write( const char* buffer, std::size_t num_chars )
     {
         if( num_chars != 0
             &&
             std::fwrite(buffer, num_chars, 1, m_file_handle) == 0 )
         {
             throw write_error() ;
         }
     }
 
 private:
     std::FILE* m_file_handle ;
 
     // copy and assignment not implemented; prevent their use by
     // declaring private.
     file( const file & ) ;
     file & operator=( const file & ) ;
 } ;

This RAII class can be used as follows :

void example_with_RAII()
{
  // open file (acquire resource)
  file logfile("logfile.txt") ;
 
  logfile.write("hello logfile!") ;
  // continue writing to logfile.txt ...
 
  // logfile.txt will automatically be closed because logfile's
  // destructor is always called when example_with_RAII() returns or
  // throws an exception.
}

Without using RAII, each function using an output log would have to manage the file explicitly. For example, an equivalent implementation without using RAII is this:

void example_without_RAII()
{
  // open file
  std::FILE* file_handle = std::fopen("logfile.txt", "w+") ;
 
  if( file_handle == NULL )
  {
    throw open_error() ;
  }
 
  try
  {
 
    if( std::fputs("hello logfile!", file_handle) == EOF )
    {
      throw write_error() ;
    }
 
    // continue writing to logfile.txt ... do not return
    // prematurely, as cleanup happens at the end of this function
  }
  catch(...)
  {
    // manually close logfile.txt
    std::fclose(file_handle) ;
 
    // re-throw the exception we just caught
    throw ;
  }
 
  // manually close logfile.txt 
  std::fclose(file_handle) ;
}

The implementation of file and example_without_RAII() becomes more complex if fopen() and fclose() could potentially throw exceptions; example_with_RAII() would be unaffected, however.

The essence of the RAII idiom is that the class file encapsulates the management of any finite resource, like the FILE* file handle. It guarantees that the resource will properly be disposed of at function exit. Furthermore, file instances guarantee that a valid log file is available (by throwing an exception if the file could not be opened).

There's also a big problem in the presence of exceptions: in example_without_RAII(), if more than one resource were allocated, but an exception was to be thrown between their allocations, there's no general way to know which resources need to be released in the final catch block - and releasing a not-allocated resource is usually a bad thing. RAII takes care of this problem; the automatic variables are destructed in the reverse order of their construction, and an object is only destructed if it was fully constructed (no exception was thrown inside its constructor). So example_without_RAII() can never be as safe as example_with_RAII() without special coding for each situation, such as checking for invalid default values or nesting try-catch blocks. Indeed, it should be noted that example_without_RAII() contained resource bugs in previous versions of this article.

This frees example_with_RAII() from explicitly managing the resource as would otherwise be required. When several functions use file, this simplifies and reduces overall code size and helps ensure program correctness.

example_without_RAII() resembles the idiom used for resource management in non-RAII languages such as Java. While Java's try-finally blocks allow for the correct release of resources, the burden nonetheless falls on the programmer to ensure correct behavior, as each and every function using file may explicitly demand the destruction of the log file with a try-finally block.