Who needs exceptions anyways?
Two things inspired this article:
Exceptions are supported in many programming languages. Therefore it is not surprising that a lot of code has been developed that uses exceptions for error reporting. What does it mean to those of us who do not make use of exceptions? It means that everyone is forced to catch! If you are using C++, the chances are that you had to put in a few catch statements to deal with exceptions being thrown from various STL containers. Some MFC classes report errors by throwing exceptions. The moral is – you can run but you cannot hide, i.e. it is necessary to catch exceptions in order to make use of the libraries that throw.
Definitions:
Code
that preserves object’s state in the event of exception being thrown is called exception-safe.
Code that guarantees to propagate any exceptions that were not intended for that code is called exception-neutral.
OK, so catching is unavoidable. But is it really necessary to use exceptions instead of old good return values? After all, exceptions come with a price tag –the code must be exception-safe and exception-neutral. And that is a big headache! Lets conduct an experiment to prove that good code can be written without worrying about exception safety.
For simplicity reasons the implementations for ConcreteAccount::Withdraw and ConcreteAccount::Deposit are omitted. It is assumed that both Withdraw and Deposit may return false in certain cases.
class BankAccount
{
public:
virtual
double GetBalance () const = 0;
virtual
bool Withdraw (double dAmount) = 0;
virtual
bool Deposit (double dAmount) = 0;
virtual
bool Transfer (double dAmount, BankAccount & tFrom) = 0;
};
class ConcreteAccount : public BankAccount
{
public:
ConcreteAccount
(double dOpeningBalance = 0)
:
m_dBalance (dOpeningBalance)
{}
virtual
~ConcreteAccount () {}
double
GetBalance () const { return m_dBalance; }
bool
Withdraw (double dAmount);
bool
Deposit (double dAmount);
bool
Transfer (double dTransferAmount, BankAccount & tFrom);
private:
// Since
I am lazy I am not going to implement the
// copy
constructor and the assignment operator... ;)
ConcreteAccount
(const ConcreteAccount &);
ConcreteAccount
& operator= (const ConcreteAccount &);
double
m_dBalance;
};
bool ConcreteAccount::Transfer (double
dTransferAmount, BankAccount & tFrom)
{
if (this
!= &tFrom)
{
if (tFrom.GetBalance () >=
dTransferAmount)
{
Deposit (dTransferAmount);
tFrom.Withdraw (dTransferAmount);
return true;
}
}
return
false;
}
…
int main ()
{
ConcreteAccount
account1 (1000);
ConcreteAccount
account2 (1000);
double
dTransferAmount = 150.00;
bool
bRetVal = account1.Transfer (dTransferAmount, account2);
if
(bRetVal)
{
std::cout << "Transfer request
approved." << std::endl;
std::cout <<
"New destination account balance: $" << account1.GetBalance ()
<< std::endl;
std::cout <<
"New source account balance: $" << account2.GetBalance ()
<< std::endl;
}
else
{
std::cout << "Transfer request
rejected." << std::endl;
}
return
0;
}
Looks reasonable, doesn't it? There is a slight problem however with the implementation of ConcreteAccount::Transfer. The code does not check the return values from Deposit and Withdraw calls. Well, that’s easy to fix! Consider the following version of ConcreteAccount::Transfer:
bool ConcreteAccount::Transfer (double
dTransferAmount, BankAccount & tFrom)
{
if (this
!= &tFrom)
{
if (Deposit (dTransferAmount
))
{
if (tFrom.Withdraw (dTransferAmount))
{
return
true;
}
else
{
//
Undo the deposit
if
(!Withdraw (dTransferAmount))
{
// I just made myself dTransferAmount dollars!!!
}
}
}
}
return
false;
}
Now that is much better! Note that it is no longer necessary to check if there are enough funds on the source account. Withdrawal’s implementation will return false if the funds are insufficient. This is indeed wonderful, as the source account might have an overdraft protection.
Lets walk through the method. If the first call to Deposit fails, then nothing happens, and false is returned. If the call to tFrom.Withdraw fails, then the deposit is rolled back. Excellent! But what happens if the rollback call to Withdraw fails? The return value can definitely be checked, but there is no way to undo the deposit! I wish my bank had a bug like that in their software... ;)
Evidently, this approach doesn’t work. No worries! The almighty bank software developers will fix the problem in no time!
The previous example demonstrated that simply checking return values is not sufficient to achieve good error handling. The Transfer method needs to be able to rollback any changes made before any of the failure points. What if the method operated on a copy of the ConcreteAccount class? In the case of a failure such a copy would simply be destroyed without affecting the original account instance. Excellent! At the end the values from the copy would be moved back into the account instance.
class ConcreteAccount : public BankAccount
{
public:
ConcreteAccount
(double dOpeningBalance = 0)
:
m_dBalance (dOpeningBalance)
{}
ConcreteAccount
(const ConcreteAccount & crtThat)
:
m_dBalance (crtThat.m_dBalance)
{}
virtual
~ConcreteAccount () {}
ConcreteAccount
& operator= (const ConcreteAccount & crtThat);
double
GetBalance () const { return m_dBalance; }
bool Withdraw
(double dAmount);
bool
Deposit (double dAmount);
bool
Transfer (double dTransferAmount, BankAccount & tFrom);
private:
void
SafeCopyFrom (const ConcreteAccount & crtThat);
double
m_dBalance;
};
ConcreteAccount & ConcreteAccount::operator=
(const ConcreteAccount & crtThat)
{
if (this
!= &crtThat)
{
SafeCopyFrom (crtThat);
}
return
*this;
}
void ConcreteAccount::SafeCopyFrom (const
ConcreteAccount & crtThat)
{
// This
method guarantees that the copy operation will succeed.
m_dBalance
= crtThat.m_dBalance;
}
bool ConcreteAccount::Transfer (double
dTransferAmount, BankAccount & tFrom)
{
if (this
!= &tFrom)
{
// Create account copy
ConcreteAccount tTemp (*this);
if (tTemp.Deposit (dTransferAmount))
{
if (tFrom.Withdraw (dTransferAmount))
{
//
At this point everything is OK
SafeCopyFrom
(tTemp);
return
true;
}
}
}
return false;
}
…
int main ()
{
ConcreteAccount
account1 (1000);
ConcreteAccount
account2 (1000);
double
dTransferAmount = 150.00;
bool
bRetVal = account1.Transfer (dTransferAmount, account2);
if
(bRetVal)
{
std::cout << "Transfer request
approved." << std::endl;
std::cout <<
"New destination account balance: $" << account1.GetBalance ()
<< std::endl;
std::cout <<
"New source account balance: $" << account2.GetBalance ()
<< std::endl;
}
else
{
std::cout << "Transfer request
rejected." << std::endl;
}
return
0;
}
There are three new methods on the ConcreteAccount class: a copy constructor, an assignment operator and a new method with a funny name SafeCopyFrom.
The implementation of ConcreteAccount::SafeCopyFrom does very little – it simply copies a double value from the source account instance. It is obvious that this method simply cannot fail. ConcreteAccount::Transfer method utilizes this fact. All the changes are made in a temporary instance of ConcreteAccount class. When the balance on the source account is successfully reduced, SafeCopyFrom is used to update the current account instance with the changes from the temporary account.
Mission accomplished!
Let’s summarize the results of our little experiment:
The last bullet point is of the utmost importance! It says that in order to make the code work we had to make ConcreteAccount::Transfer method exception-safe! Indeed, assuming that the implementation of Withdraw method is also exception safe, i.e. it either succeeds or all the changes are rolled back, if an exception were to be thrown anywhere in the method before the call to SaveCopyFrom, the state of the account would not change. Since SaveCopyFrom is very simply, it could be redefined as:
void
SafeCopyFrom (const ConcreteAccount & crtThat) throw ();
This means that starting from the call to SaveCopyFrom the Transfer method cannot possibly throw. So much for not wanting to deal with exception safety!
Another interesting side effect is that our program did not contain a single catch statement. That means that our code is exception neutral! Going to such a great length to avoid exceptions led us to writing exception-safe and exception-neutral code! Ouch!
Our experiment demonstrated that every system that deals with error conditions has a moral equivalent of exception-safety. Guaranteeing object’s integrity is a fundamental requirement for any system.
So is it worth to continue fighting the exceptions? Lets re-write our little program to use exceptions and compare the results.
#include <iostream>
class BankAccount
{
public:
virtual
double GetBalance () const = 0;
virtual
void Withdraw (double dAmount) = 0;
virtual
void Deposit (double dAmount) = 0;
virtual
void Transfer (double dAmount, BankAccount & tFrom) = 0;
};
class ConcreteAccount : public BankAccount
{
public:
ConcreteAccount
(double dOpeningBalance = 0)
:
m_dBalance (dOpeningBalance)
{}
ConcreteAccount
(const ConcreteAccount & crtThat)
:
m_dBalance (crtThat.m_dBalance)
{}
virtual
~ConcreteAccount () {}
ConcreteAccount
& operator= (const ConcreteAccount & crtThat);
double
GetBalance () const { return m_dBalance; }
void
Withdraw (double dAmount);
void
Deposit (double dAmount);
void
Transfer (double dTransferAmount, BankAccount & tFrom);
private:
void
SafeCopyFrom (const ConcreteAccount & crtThat);
double
m_dBalance;
};
ConcreteAccount & ConcreteAccount::operator=
(const ConcreteAccount & crtThat)
{
if (this
!= &crtThat)
{
SafeCopyFrom (crtThat);
}
return
*this;
}
void ConcreteAccount::SafeCopyFrom (const
ConcreteAccount & crtThat)
{
// This
method guarantees that the copy operation will succeed.
m_dBalance
= crtThat.m_dBalance;
}
void ConcreteAccount::Withdraw (double dAmount)
{
if
(dAmount > m_dBalance)
{
throw std::string ("Withdrawal
denied: Insufficient funds.");
}
m_dBalance
-= dAmount;
}
void ConcreteAccount::Deposit (double dAmount)
{
m_dBalance
+= dAmount;
}
void ConcreteAccount::Transfer (double
dTransferAmount, BankAccount & tFrom)
{
if (this
== &tFrom)
{
throw std::string
("Transfers between two instances of the same account are not
allowed.");
}
//
Create account copy
ConcreteAccount
tTemp (*this);
tTemp.Deposit
(dTransferAmount);
tFrom.Withdraw
(dTransferAmount);
// At
this point everything is OK
SafeCopyFrom
(tTemp);
}
int main ()
{
try
{
ConcreteAccount account1 (1000);
ConcreteAccount account2 (1000);
double dTransferAmount = 150.00;
account1.Transfer (dTransferAmount,
account2);
std::cout << "Transfer request
approved." << std::endl;
std::cout <<
"New destination account balance: $" << account1.GetBalance ()
<< std::endl;
std::cout <<
"New source account balance: $" << account2.GetBalance ()
<< std::endl;
}
catch
(std::string ex)
{
std::cout << ex.c_str () <<
std::endl;
}
return
0;
}
Note the following:
Disclaimer: There are many tricky issues associated with exception handling. For more information refer to one of the following books:
· The C++ Programming Language Special Edition, Bjarne Stroustrup
· Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions, Herb Sutter