Constructors
When
writing code with exceptions, it’s particularly important that you always
be asking, “If an exception occurs, will this be properly cleaned
up?” Most of the time you’re fairly safe, but in constructors
there’s a problem: If an exception is thrown before a constructor is
completed, the associated destructor will not be called for that object. This
means you must be especially diligent while writing your constructor.
The
general difficulty is allocating resources in constructors. If an exception
occurs in the constructor, the destructor doesn’t get a chance to
deallocate the resource. This problem occurs most often with
“naked” pointers.
For example,
//: C23:Nudep.cpp
// Naked pointers
#include <fstream>
#include <cstdlib>
using namespace std;
ofstream out("nudep.out");
class Cat {
public:
Cat() { out << "Cat()" << endl; }
~Cat() { out << "~Cat()" << endl; }
};
class Dog {
public:
void* operator new(size_t sz) {
out << "allocating an Dog" << endl;
throw int(47);
return 0;
}
void operator delete(void* p) {
out << "deallocating an Dog" << endl;
::delete p;
}
};
class UseResources {
Cat* bp;
Dog* op;
public:
UseResources(int count = 1) {
out << "UseResources()" << endl;
bp = new Cat[count];
op = new Dog;
}
~UseResources() {
out << "~UseResources()" << endl;
delete []bp; // Array delete
delete op;
}
};
int main() {
try {
UseResources ur(3);
} catch(int) {
out << "inside handler" << endl;
}
} ///:~
The
output is the following:
UseResources()
Cat()
Cat()
Cat()
The
UseResources
constructor is entered, and the
Cat
constructor is successfully completed for the array objects. However, inside
Dog::operator
new
,
an exception is thrown (as an example of an out-of-memory error). Suddenly, you
end up inside the handler,
without
the
UseResources
destructor being called. This is correct because the
UseResources
constructor was unable to finish, but it means the
Cat
object that was successfully created on the heap is never destroyed.
Making
everything an object
To
prevent this, guard against these “raw” resource allocations by
placing the allocations inside their own objects with their own constructors
and destructors. This way, each allocation becomes atomic,
as an object, and if it fails, the other resource allocation objects are
properly cleaned up. Templates are an excellent way to modify the above example:
//: C23:Wrapped.cpp
// Safe, atomic pointers
#include <fstream>
#include <cstdlib>
using namespace std;
ofstream out("wrapped.out");
// Simplified. Yours may have other arguments.
template<class T, int sz = 1> class PWrap {
T* ptr;
public:
class RangeError {}; // Exception class
PWrap() {
ptr = new T[sz];
out << "PWrap constructor" << endl;
}
~PWrap() {
delete []ptr;
out << "PWrap destructor" << endl;
}
T& operator[](int i) throw(RangeError) {
if(i >= 0 && i < sz) return ptr[i];
throw RangeError();
}
};
class Cat {
public:
Cat() { out << "Cat()" << endl; }
~Cat() { out << "~Cat()" << endl; }
void g() {}
};
class Dog {
public:
void* operator new[](size_t sz) {
out << "allocating an Dog" << endl;
throw int(47);
return 0;
}
void operator delete[](void* p) {
out << "deallocating an Dog" << endl;
::delete p;
}
};
class UseResources {
PWrap<Cat, 3> Bonk;
PWrap<Dog> Og;
public:
UseResources() : Bonk(), Og() {
out << "UseResources()" << endl;
}
~UseResources() {
out << "~UseResources()" << endl;
}
void f() { Bonk[1].g(); }
};
int main() {
try {
UseResources ur;
} catch(int) {
out << "inside handler" << endl;
} catch(...) {
out << "inside catch(...)" << endl;
}
} ///:~
The
difference is the use of the template to wrap the pointers and make them into
objects. The constructors for these objects are called
before
the body of the
UseResources
constructor, and any of these constructors that complete before an exception is
thrown will have their associated destructors called.
The
PWrap
template shows a more typical use of exceptions than you’ve seen so far:
A nested class called
RangeError
is created to use in
operator[
]
if its argument is out of range. Because
operator[
]
returns a reference
it cannot return zero. (There are no null references.) This is a true
exceptional condition – you don’t know what to do in the current
context, and you can’t return an improbable value. In this example,
RangeError
is very simple and assumes all the necessary information is in the class name,
but you may also want to add a member that contains the value of the index, if
that is useful.
Cat()
Cat()
Cat()
PWrap constructor
allocating a Dog
~Cat()
~Cat()
~Cat()
PWrap destructor
inside handler
Again,
the storage allocation for
Dog
throws an exception, but this time the array of
Cat
objects is properly cleaned up, so there is no memory leak.
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru