How to use RAII to acquire resources of class?
Asked Answered
D

6

7

There is example that shows that using RAII this way:

class File_ptr{
//...
   File* p;
   int* i; 
   public:
      File_ptr(const char* n, const char* s){
         i=new int[100];
         p=fopen(n,a); // imagine fopen might throws
      }
      ~File_ptr(){fclose(p);}
}

void use_file(const char* fn){
    File_ptr(fn,"r");
}

is safe. but my question is: what if there is exception thrown in p=fopen(n,a); then memory allocated to i is not returned. Is this right to assume that RAII tells you then each time you want X to be safe then all resources acquired by X must be allocated on stack? And if X.a is being created then resources of a must also be placed on stack? and again, and again, I mean finally if there is some resource placed on heap how it could be handled with RAII? If it is not mine class i.e.

Dusen answered 24/4, 2013 at 19:7 Comment(8)
Separate your concerns, have one resource managing class per resource and one class aggregating managed resources.Retirement
fopen is a C function so it won't throw any exception because C doesn't have exceptionAqualung
it is example, imagine it throwsDusen
The fix is to change int* i to something like std::vector<int> i. That is, to use RAII.Sleuth
If you had a function that throws an exception you have two options handle the exception or keep throwing it up. Its fine to have a constructor that throws.Motivate
I know vector solves it, but RAII is not dependent on stlDusen
It is dependent on the Rule of Three or Five, though.Stickweed
@authority: You could use a smart pointer or any class object that cleans up the memory in the destructor.Dimidiate
R
1

Treating this as an intellectual exercise where you don't want to use std::vector, you need to divide your classes up so they have a single responsibility. Here's my "integer array" class. Its responsibility is to manage the memory for an integer array.

class IntArray {
public:
    IntArray() : ptr_(new int[100]) {}
    ~IntArray() { delete[] ptr_; }
    IntArray(const IntArray&) = delete; // making copyable == exercise for reader
    IntArray& operator=(const IntArray&) = delete;
    // TODO: accessor?
private:
    int* ptr_;
};

Here is my file handling class. Its responsibility is to manage a FILE*.

class FileHandle {
public:
    FileHandle(const char* name, const char* mode)
     : fp_(fopen(name, mode))
    {
        if (fp_ == 0)
            throw std::runtime_error("Failed to open file");
    }
    ~FileHandle() {
        fclose(fp_); // squelch errors
    }
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    // TODO: accessor?
private:
    FILE* fp_;
};

Note, that I convert my construction error to an exception; fp_ being a valid file pointer is an invariant that I wish to maintain so I abort construction if I cannot set this invariant up.

Now, makeing File_ptr exception safe is easy and the class needs no complex resource management.

class File_ptr {
private:
    FileHandle p;
    IntArray i; 
public:
    File_ptr(const char* n, const char* s)
     : p(n, s)
     , i()
    {}
};

Note the lack of any user-declared destructor, copy assignment operator or copy constructor. I can swap the order of the members and in either case it doesn't matter which constructor throws.

Retirement answered 25/4, 2013 at 11:11 Comment(7)
thanks. what if in your File_ptr class constructor i() throws? destructor ~IntArray() for vaiable File_ptr.i will be called because it is on stack, no matter than constructor failed and it is not considered initialized? is it considered constructed though? I have seen that constructor must return to consider object as createdDusen
@authority: If IntArray() throws during the construction of i then p will be cleaned up as the first part of stack unwinding. (Because the initialization of i is ordered after the initialization of p, we know that p must have been successfully initialized by the time the initialization of i throws an exception. This means that p can and must be cleaned up.)Retirement
yes, and my question is what with memory allocated by IntArray, delete[]ptr_ is not called then. And in generall this is again same case: IntArray should be constructed like File_ptr to allocate on stack, so objects are for sure deleted even when it throws, right? I mean at the last end I never escpape from doing something that will not be deleted (unless I use try(){}catch() ofcourse)Dusen
please explain me this, this is essence of my questionDusen
@authority: There is no memory allocated by IntArray in that situation; it threw an exception! That is why it is so fundamentally important to have you resource managing classes manage one resource each.Retirement
ok, another issue: assume it was this memory allocated - object IntArray i in class File_ptr is not considered constructed, so File_ptr is not considered constructed: is destructor of File_ptr called or not?Dusen
No, the destructor ~File_ptr is only called on instances of File_ptr that are fully constructed. If any part of the construction of the File_ptr fails then the destructor will not be called on that object.Retirement
P
11

The whole point of RAII is to NOT assign any resources (like the int-array) to dangling pointers. Instead, use std::vector or assign the array pointer to something like std::unique_ptr. This way the resources will be destroyed as exceptions occur.

And no, you don't have to use STL, but to make use of RAII, the lowest base-resources (like heap allocated arrays) have to be created using RAII as well and the easiest way to do this is to use STL rather than writing your own smart-pointer or vectors.

Perilymph answered 24/4, 2013 at 19:32 Comment(4)
Unfortunately the standard library doesn't provide any RAII facilities except for memory. They should have at least included something for files.Vulgus
Well std::fstream does just that. But one could argue that a stream is not exactly the same as a simple filehandle. With C++11, threading based objects like mutex and lock are added. But yeah, STL is a bit lacking in comparison to other frameworks. It's more like a best practice-collection of extended core language features.Perilymph
As an aside, you probably have never used the STL, which is the name of the library that inspired many features of the std library.Varela
@Yakk: You're right, how come I have never heard of that. But it seems STL is used very often as a synonym for the std-library since std-library is in fact a std-named, template-based, library. Besides, OP mentioned STL in one of the comments.Perilymph
V
3

if exception happen after the new, you have to catch the exception and delete the pointer in the constructor then re-throw in this case, the destructor will not be called since the object is never constructed.

otherwise if i is a std::vector, it will clean up automatically

Valenevalenka answered 24/4, 2013 at 19:15 Comment(2)
I know vector solves it, but RAII is not dependent on stl. What in general caseDusen
@authority in the general case, use one RAII object per resource. Vectors are just one example of such a type.Sussex
V
3

One way to handle this is to put everything that might be invalidated by an exception into a local variable which itself uses RAII, then assign to your members at the end when it's safe.

class File_ptr{
//...
   File* p;
   int* i; 
   public:
      File_ptr(const char* n, const char* s) i(NULL), p(NULL) {
         unique_ptr<int> temp_i=new int[100];  // might throw std::bad_alloc
         p=fopen(n,a); // imagine fopen might throws
         // possibility of throwing an exception is over, safe to set members now
         i = temp_i.release();
      }
      ~File_ptr(){fclose(p);}
}

For more information see Exception Safety.

Vulgus answered 24/4, 2013 at 19:47 Comment(0)
M
2

If you know it throws, put it in a try-catch.

File_ptr(const char* n, const char* s) {
    i=new int[100];
    try {
        p=fopen(n,a); // imagine fopen might throws
    } catch(...) {
         delete[] i;
         throw;
    }
}
Motivate answered 24/4, 2013 at 19:21 Comment(1)
I think the original exception should be rethrown.Algar
H
1
File_ptr(const char* n, const char* s)
{
  std::unique_ptr<int[]> sp(new int[100]);
  p = fopen(n, s);
  i = sp.release();
}
Hubie answered 24/4, 2013 at 19:53 Comment(0)
R
1

Treating this as an intellectual exercise where you don't want to use std::vector, you need to divide your classes up so they have a single responsibility. Here's my "integer array" class. Its responsibility is to manage the memory for an integer array.

class IntArray {
public:
    IntArray() : ptr_(new int[100]) {}
    ~IntArray() { delete[] ptr_; }
    IntArray(const IntArray&) = delete; // making copyable == exercise for reader
    IntArray& operator=(const IntArray&) = delete;
    // TODO: accessor?
private:
    int* ptr_;
};

Here is my file handling class. Its responsibility is to manage a FILE*.

class FileHandle {
public:
    FileHandle(const char* name, const char* mode)
     : fp_(fopen(name, mode))
    {
        if (fp_ == 0)
            throw std::runtime_error("Failed to open file");
    }
    ~FileHandle() {
        fclose(fp_); // squelch errors
    }
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    // TODO: accessor?
private:
    FILE* fp_;
};

Note, that I convert my construction error to an exception; fp_ being a valid file pointer is an invariant that I wish to maintain so I abort construction if I cannot set this invariant up.

Now, makeing File_ptr exception safe is easy and the class needs no complex resource management.

class File_ptr {
private:
    FileHandle p;
    IntArray i; 
public:
    File_ptr(const char* n, const char* s)
     : p(n, s)
     , i()
    {}
};

Note the lack of any user-declared destructor, copy assignment operator or copy constructor. I can swap the order of the members and in either case it doesn't matter which constructor throws.

Retirement answered 25/4, 2013 at 11:11 Comment(7)
thanks. what if in your File_ptr class constructor i() throws? destructor ~IntArray() for vaiable File_ptr.i will be called because it is on stack, no matter than constructor failed and it is not considered initialized? is it considered constructed though? I have seen that constructor must return to consider object as createdDusen
@authority: If IntArray() throws during the construction of i then p will be cleaned up as the first part of stack unwinding. (Because the initialization of i is ordered after the initialization of p, we know that p must have been successfully initialized by the time the initialization of i throws an exception. This means that p can and must be cleaned up.)Retirement
yes, and my question is what with memory allocated by IntArray, delete[]ptr_ is not called then. And in generall this is again same case: IntArray should be constructed like File_ptr to allocate on stack, so objects are for sure deleted even when it throws, right? I mean at the last end I never escpape from doing something that will not be deleted (unless I use try(){}catch() ofcourse)Dusen
please explain me this, this is essence of my questionDusen
@authority: There is no memory allocated by IntArray in that situation; it threw an exception! That is why it is so fundamentally important to have you resource managing classes manage one resource each.Retirement
ok, another issue: assume it was this memory allocated - object IntArray i in class File_ptr is not considered constructed, so File_ptr is not considered constructed: is destructor of File_ptr called or not?Dusen
No, the destructor ~File_ptr is only called on instances of File_ptr that are fully constructed. If any part of the construction of the File_ptr fails then the destructor will not be called on that object.Retirement

© 2022 - 2024 — McMap. All rights reserved.