Topics Covered:
- Introduction.
- Stack unwinding.
- Stack unwinding and dynamic memory allocation.
- Nested exception handling.
- Error handling in constructor and destructor.
- noexcept keyword in c++.
1. Introduction:
It is a mechanism of handling errors in a program during runtime.
- Required immediate handling.
- If not handled program crashes.
- Return code of errors can be ignaored not exceptions
Note:
- Below are the list of classes derived from base class ‘exception’
out_of_range
bad_array_new_length
runtime_error - If none of the above classes matched base class ‘exception’ is handled.
- For handling all type of exceptions use (…) in catch, it should be avoided as it will not have any exception information\object.
int processRecord(int size)
{
if (size < 10) //case 1: throwing an exception
throw std::out_of_range("size should be greater than 0");
int *ptrArray1 = new int[size]; //case 2. results in an exception for a larger value. (std::bad_array_new_length )
int *ptrArray2 = (int*)malloc(size * sizeof(size));
if (ptrArray2 == NULL) //case 3: if malloc failed to allocate memory throw an exception.
throw runtime_error("unable to allocate memory");
}
int main()
{
try
{
// processRecord(9); // case 1
processRecord(std::numeric_limits<int>::max()); // case 2
}
catch (std::bad_array_new_length &ex)
{
cerr << ex.what() << endl;
}
catch (runtime_error &ex)
{
cerr << ex.what() << endl;
}
catch (out_of_range &ex) // case 1
{
cerr << ex.what() << endl;
}
catch (exception &ex)
{
cerr << ex.what() << endl;
}
return 1;
}
2. Stack unwinding:
Stack unwinding is a process that is based on RAII(Resource Acquisition Is Initialization), where an object created on the stack gets destroyed once goes out of scope.
2. Destructor gets called once Test object goes of the scope.
class Test
{
public:
Test()
{
cout << "Constructor of Test" << endl;
}
~Test()
{
cout << "Destructor of Test" << endl;
}
};
int processRecord(int size)
{
Test t;
if (size < 10)
throw std::out_of_range("size should be greater than 0");
cout << "Step after exception" << endl;
return 0;
}
int main()
{
try
{
processRecord(9);
}
catch (out_of_range &ex)
{
cerr << ex.what() << endl;
}
return 1;
}
Output:
Constructor of Test
Destructor of Test
size should be greater than 0
3. Stack unwinding and dynamic memory allocation:
In case of dynamic memory allocation, and when the exception is thrown then the delete will not be executed.
Note: The Test pointer is allocated on the heap and when there is an exception in the constructor then the delete will never get called.
class Test
{
public:
Test()
{
cout << "Constructor of Test" << endl;
}
~Test()
{
cout << "Destructor of Test" << endl;
}
};
int processRecord(int size)
{
Test *t = new Test();
if (size < 10)
throw std::out_of_range("size should be greater than 0");
cout << "Step after exception" << endl;
delete t;
return 0;
}
int main()
{
try
{
processRecord(9);
}
catch (out_of_range &ex)
{
cerr << ex.what() << endl;
}
return 1;
}
Output:
Constructor of Test
size should be greater than 0
Solution to unwinding in case of dynamic memory allocation:
- Allocate memory using a smart pointer (RAII). and thereby when the object goes out of scope destructor gets called and memory will be released.
int processRecord(int size)
{
unique_ptr t (new Test());
if (size < 10)
throw std::out_of_range("size should be greater than 0");
cout << "Step after exception" << endl;
return 0;
}
Output:
Constructor of Test
Destructor of Test
size should be greater than 0
4. Nested exception handling:
When exception is thrown, it will be handled by parent catch block. The same exception can also be handled or sent to outer exception block( Nested Exception).
Example code explained for clear understanding.
1. bernoulli_distribution helps random boolean probability.
2. Random generation record continues until it reaches error count 3. 3. As soon as the error count reaches 3, it throws exceptions and record processing stops.
int processRecord(int size)
{
vector vec;
vec.reserve(size);
for (int i=0; i < size; i++)
vec.push_back(i);
std::default_random_engine eng;
std::bernoulli_distribution Bar;
int error_count = {};
for (int i=0; i < size; i++)
{
try
{
if (!Bar(eng))
{
error_count++;
throw runtime_error("Error in reading records");
}
cout << "Record proccesing :" << vec[i] << endl;
}
catch (runtime_error &ex)
{
if (error_count > 2)
{
//throw; Throw same runtime_error exception from above
runtime_error e = runtime_error("Exception from nested catch");
ex = e;
throw ex;
}
cerr << ex.what() << " : " << i << endl;
}
cout << "Continued processing record : " << endl;
}
return 0;
}
int main()
{
try
{
processRecord(100);
}
catch (runtime_error &ex)
{
cerr << ex.what() << endl;
}
return 1;
}
Output:
Record processing :0
Continued processing record :
Error in reading records: 1
Continued processing record :
Error in reading records: 2
Continued processing record :
Record processing :3
Continued processing record :
Record processing:4
Continued processing record :
Exception from nested catch
5. Error handling in constructor and destructor:
1. Lets take an example where pointer of type A is allocated memory on heap
2. As soon as the Test class object goes out of scope, the destructor of both classes A and B gets invoked.
class A
{
public:
A()
{
cout << "Ctor of class A" << endl;
}
~A()
{
cout << "Dtor of class A" << endl;
}
};
class B
{
public:
B()
{
cout << "Ctor of class B" << endl;
}
~B()
{
cout << "Dtor of class B" << endl;
}
};
class Test
{
A *ptrA;
B objB{};
public:
Test()
{
cout << "Ctor of class Test" << endl;
ptrA = new A();
}
~Test()
{
cout << "Dtor of class Test" << endl;
delete ptrA;
}
};
int main()
{
try
{
Test t;
}
catch (runtime_error &ex)
{
cerr << ex.what() << endl;
}
return 1;
}
Output:
Constructor of class B
Constructor of class Test
Constructor of class A
Destructor of class Test
Destructor of class A
Destructor of class B
Note:
1. When an exception is thrown in the Test constructor, then note that the destructor of class A is not get invoked.
2. To overcome this situation, make sure dynamic memory should always be created using RAII ( smart pointer).
6. noexcept in c++:
- Tell the compiler that function does not throw any exception and thereby it avoids stack unwinding.
- If noexcept function throws an exception, then an exception cannot be catch and it leads to program termination.
- noexcept (true) by default.
- After C++ 11, all destructors are noexcept( true) by default.
- The compiler gives a warning if noexcept function throws an exception.
void Sum(int x, int y) noexcept
{
cout << "Sum : " << x + y << endl;
}
int main()
{
try
{
Sum(10, 20);
}
catch (runtime_error &ex)
{
cerr << ex.what() << endl;
}
return 1;
}
Problem with noexcept: Program termination due to invoking function throws an exception
Below program terminates as function Sum is marked as noexcept, but there is exception thrown from Test function.
void Test() noexcept(false)
{
throw runtime_error("exception in Test");
}
void Sum(int x, int y) noexcept
{
Test();
cout << "Sum : " << x + y << endl;
}
int main()
{
try
{
Sum(10, 20);
}
catch (runtime_error &ex)
{
cerr << ex.what() << endl;
}
return 1;
}
Solution:
Check noexcept explicitly for invoking function. i.e. noexcept(noexcept(Test())
void Test() noexcept(false)
{
throw runtime_error("exception in Test");
}
void Sum(int x, int y) noexcept(noexcept(Test()))
{
Test();
cout << "Sum : " << x + y << endl;
}
int main()
{
try
{
Sum(10, 20);
}
catch (runtime_error &ex)
{
cerr << ex.what() << endl;
}
return 1;
}