Who needs exceptions anyways?

Foreword

Two things inspired this article:

To catch, or not to catch: that is a stupid question:

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.

To throw, or not to throw: that is the question:

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. 

Goals

Implementation #1

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!

Implementation #2

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!

 

Summary

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!

Life after return values

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:

Conclusions

 

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