Joel Spolsky does not like programming with exceptions. Not only that, his recommended policy seems to be
- Never throw an exception of my own;
- Always catch any possible exception that might be thrown by a library I'm using on the same line as it is thrown and deal with it immediately.
In this article, we contend that this policy should never be used in a modern C++ design. Actually, we will see why the recommended policy will be:
- Always throw an exception in case of a catastrophic or completely unexpected condition, like a class invariant being violated.
- Always enclose the main body of your program within a try/catch block that catches all kinds of exceptions.
- Refrain from writing try/catch blocks in your application. Most of the time the main exception handler should be able to take care of the exception and let the program continue to run.
There are quite a few other Error and Exception Handling recommendations, but they are beyond the scope of this article.
Incidentally, somebody else already came up with Exceptions Not Considered Harmful so I had to use a slightly different title, not mentioning to write a rather more urbane rebuttal.
Catch or let go?
Most people who agree with me on this topic and have been reading Joel's articles since the inception of Joel On Software may be asking themselves why in the world a smart guy like him does not grok exceptions. The answer, I believe, is simple: Joel, by his public admission, uses prevalently Visual Studio with MFC. Had I used MFC exclusively myself, perhaps I would have agreed with him; however, at my previous job, luckily I had to use Borland C++ Builder for our applications. C++ Builder automatically encloses the main body of the program within a try/catch block, and one quickly discovers that it makes life a lot easier.
In a typical scenario, let's say you want to create a simple text edit field that accepts numerical input. In Builder, you will simply drag a TEdit component onto a form and run the program. When it is time to convert the text to a number and do something with it, you will have to write the following code:
int numberOfIterations = EditNumberOfIterations->Text.ToInt();
What happens if the user enters something really nasty, like a bunch of random letters?
AnsiString::ToInt()
will throw an EConvertError
exception.
Terrified by the prospect, people not acquainted with Builder will write the following code to avoid the exception:
try {
int numberOfIterations = EditNumberOfIterations->Text.ToInt();
}
catch (EConvertError& e) {
// deal with the exception, display an error message or something
}
I have also seen people attempting to be smarter: they moved the try/catch code to a separate conversion function that returns a bool
to indicate failure.
It turns out that all this extra code is useless. The single original line of code will work just fine. In fact, when the EConvertError
exception is thrown, the main exception handler will catch it and automatically display an appropriate error message. Best of all, your program will keep running: you may now correct the input and try again. The astute reader will also realize how miserable his life is going to be if he has to check every one of those bool
return values.
The Borland VCL framework was designed with this in mind; this typically leads to much simpler code. What is also very useful is to set the symbolic debugger to always break on an exception when the exception is generated. This is also why exceptions should be limited to unusual cases: you don't want to break every now and then into the debugger.
This said, we should now worry about writing applications that survive when an exception is thrown. Incidentally, this requirement does not stem from the VCL: the "exceptional" code complexity is inherent in C++.
Our exceptional programming world
Indeed, exception safety was fully understood in the C++ community much after the C++ language was finalized. Even popular books like Scott Meyers' Effective C++, Second Edition were written in the pre-understanding era and still contain subtle bugs like this one (no wonder it is on sale at my local Barnes&Noble at 40% off):
delete m_Component;
m_Component = new CComponent();
Experienced C++ programmers will immediately spot the bug: m_Component
must be zeroed out before attempting to call new
. If new
fails, it will throw an exception (not necessarily std::bad_alloc
). If your program recovers gracefully from the exception as described above, you will later crash because m_Component
now contains a dangling pointer. (Been there, done that while developing CineProducerTM.)
For this reason, seasoned C++ programmers always have something like Stroustrup's destroy in their arsenal.
std::auto_ptr
At this point it is required that the reader is familiar with std::auto_ptr
. Thankfully, Herb Sutter explained its many purposes in life in Using auto_ptr Effectively and many other places, including the non-trivial, lesser known but no less important Unmanaged Pointers in C++: Parameter Evaluation, auto_ptr, and Exception Safety so I only have to link and recommend them, especially to my friend Duncan.
Exceptions, complex ownerships, and large-scale design
In an unfortunate twist, everything is complicated again when you use C++ in a large-scale system.
For sake of simplicity, let's suppose you have a CRobot
class that owns one CArm
and a CWheel
. In a small-scale system, you can write:
#ifndef __ROBOTINCLUDED__
#define __ROBOTINCLUDED__
#pragma once
#include "Arm.hpp"
#include "Wheel.hpp"
class CRobot
{
public:
CRobot();
virtual ~CRobot(); // throw()
private:
CArm m_Arm;
CWheel m_Wheel;
};
#endif
If in a worst-case scenario the CWheel
constructor throws, m_Arm
will be destroyed and an instance of CRobot
will never be created. In the CRobot
implementation we can always assume that we have a CArm
and a CWheel
.
As John Lakos has shown in Large-Scale C++ Software Design, this approach has nefarious consequences in a large system. The presence of #include
statements in header files will ultimately cause your build times to grow exponentially with the number of components in the system. Software veterans know just how bad this is:
A crucial observation here is that you have to run through the loop again and again to write a program, and so it follows that the faster the Edit-Compile-Test loop, the more productive you will be, down to a natural limit of instantaneous compiles. That's the formal, computer-science-y reason that computer programmers want really fast hardware and compiler developers will do anything they can to get super-fast Edit-Compile-Test loops. [...]
But as soon as you start working on a larger team with multiple developers and testers, you encounter the same loop again, writ larger (it's fractal, dude!). A tester finds a bug in the code, and reports the bug. The programmer fixes the bug. How long does it take before the tester gets the fixed version of the code? In some development organizations, this Report-Fix-Retest loop can take a couple of weeks, which means the whole organization is running unproductively. To keep the whole development process running smoothly, you need to focus on getting the Report-Fix-Retest loop tightened.
To go back to our header, in most cases one will have to do as follows:
#ifndef __ROBOTINCLUDED__
#define __ROBOTINCLUDED__
#pragma once
class CArm;
class CWheel;
class CRobot
{
public:
CRobot();
virtual ~CRobot(); // throw()
private:
CArm* m_Arm;
CWheel* m_Wheel;
};
#endif
Notice how in order to eliminate compile-time dependencies the #include
statements disappeared, to be replaced by a forward declaration.
In order to work around the lack of scalability, our implementation has to be much more complicated now. Let's assume for a second that you try the following:
#include "Robot.hpp"
CRobot::CRobot() :
m_Arm(new CArm),
m_Wheel(new CWheel)
{
}
CRobot::~CRobot()
{
delete m_Wheel;
delete m_Arm;
}
There is a memory leak lurking in this code. If new CWheel
throws, the destructor for CRobot
is never invoked, and m_Arm
is leaked.
Two-stage construction: a minor digression
In the early days of C++, utter despair drove many developers to defer full construction until later. Rather than letting exceptions follow their course, a separate init() function called explicitly some time after construction would filter away exceptions and perhaps partially construct the object and return an error.
This approach has many drawbacks and no real benefits, and I cannot think of a more authoritative critique than Bjarne Stroustrup's treatment in The C++ Programming Language, Appendix E.3.5, "Constructors and Invariants", so I won't elaborate further.
Exception-safe coding
If we are designing a non-copyable class (for example deriving CRobot
from boost::noncopyable), an acceptable solution might be to use std::auto_ptr<CArm>
and std::auto_ptr<CWheel>
as members.
If your objects must be copyable you cannot use std::auto_ptr
: since by now you should have completely memorized Herb Sutter's articles, once again I won't elaborate further.
What's the solution, then? If the copies can point to a single instance of shared CArm
and CWheel
, you may use boost::shared_ptr instead. Otherwise, you might have to write quite a bit of extra code:
#ifndef __ROBOTINCLUDED__
#define __ROBOTINCLUDED__
#pragma once
class CArm;
class CWheel;
class CRobot
{
public:
CRobot();
CRobot(const CRobot& in_source);
virtual ~CRobot(); // throw()
CRobot& operator=(const CRobot& in_source);
private:
CArm* m_Arm;
CWheel* m_Wheel;
CArm& Arm();
CWheel& Wheel();
protected:
void Swap(CRobot& inout_other); // throw()
};
#endif
The implementation:
#include "Robot.hpp"
CRobot::CRobot() :
m_Arm(NULL),
m_Wheel(NULL)
{
std::auto_ptr<CArm> exceptionSafeArm(new CArm);
std::auto_ptr<CWheel> exceptionSafeWheel(new CWheel);
// we performed all the potentially throwing operations;
// from now on, only non-throwing operations
m_Arm = exceptionSafeArm.release();
m_Wheel = exceptionSafeWheel.release();
}
CRobot::~CRobot()
{
delete m_Wheel;
delete m_Arm;
}
CRobot::CRobot(const CRobot& in_source) :
m_Arm(NULL),
m_Wheel(NULL)
{
std::auto_ptr<CArm> exceptionSafeArm(new CArm(in_source.Arm()));
std::auto_ptr<CWheel> exceptionSafeWheel(new CWheel(in_source.Wheel()));
// we performed all the potentially throwing operations;
// from now on, only non-throwing operations
m_Arm = exceptionSafeArm.release();
m_Wheel = exceptionSafeWheel.release();
}
CRobot& CRobot::operator=(const CRobot& in_source)
{
CRobot temp(in_source);
// we performed all the potentially throwing operations;
// from now on, only non-throwing operations
Swap(temp);
return *this;
}
void CRobot::Swap(CRobot& inout_other) // throw()
{
std::swap(m_Arm, inout_other.m_Arm);
std::swap(m_Wheel, inout_other.m_Wheel);
}
CArm& CRobot::Arm()
{
ASSERT<std::runtime_error>(m_Arm, "m_Arm NULL");
return *m_Arm;
}
CWheel& CRobot::Wheel()
{
ASSERT<std::runtime_error>(m_Wheel, "m_Wheel NULL");
return *m_Wheel;
}
In the presence of member arrays, overloaded constructors, derived classes and parameters to constructors things are even more complicated but you may use a similar technique.
Of course my most well-read guests will object that the copying code can be avoided by using the smart ValuePtr described by Herb Sutter in Item 31, More Exceptional C++. First off, I remember considering it when I had to implement a real system (not the simple CRobot above), but in my case it was not an option. Sadly, I don't remember why at the moment; the fact that I lent the book to a colleague does not help either!
Further reading
- John Lakos: Large-Scale C++ Software Design
- Bjarne Stroustrup: The C++ Programming Language (Special Edition)
- Herb Sutter: Exceptional C++
- Herb Sutter: More Exceptional C++
Disclaimer
I hastily wrote the above code during a weekend. I wouldn't be surprised if it does not compile for one reason or another and you shouldn't be either.