Robert Schmidt
October 21, 1999
Part 10 of this series on exception handling discusses exceptions arising from private subobjects.
For several columns now, I've shown you techniques for capturing the exceptions that objects may throw during their construction. All of these techniques manage exceptions after they've escaped their offending constructors. Sometimes the caller needs to know about such exceptions, but often -- as in the examples I've been showing -- the actual exception erupts from a private subobject that the user shouldn't care about. Making client code pay for the sins of "invisible" objects betrays fragile design.
Historically, implementers of (possibly throwing) constructors had no simple and robust solution. Consider this simple example:
#include <stdlib.h>
class buffer
{
public:
explicit buffer(size_t);
~buffer();
private:
char *p;
};
buffer::buffer(size_t const count)
: p(new char[count])
{
}
buffer::~buffer()
{
delete[] p;
}
static void do_something_with(buffer &)
{
}
int main()
{
buffer b(100);
do_something_with(b);
return 0;
}
buffer's constructor accepts the number of characters (count) to be allocated from the free store, then initializes buffer::p to reference that allocated storage. If the allocation fails, the constructor's new expression manifests an exception that the buffer client (main, in this instance) must catch.
Unfortunately, catching the exception is not an easy proposition. Since the throw will come from buffer::buffer, all buffer constructor calls should be wrapped in a try block. The no-brainer solution
try
{
buffer b(count);
}
catch (...)
{
abort();
}
do_something_with(b); // ERROR. At this point,
// 'b' no longer exists
won’t work. Instead, the do_something_with call must appear in the try block:
try
{
buffer b(100);
do_something_with(b);
}
catch (...)
{
abort();
}
do_something_with(b);
(To stave off nasty-grams: I know that calling abort is a tacky way to handle this exception. I'm using abort as a placeholder, since my focus here is catching the exception, not actually recovering from it.)
While somewhat awkward, this solution does work. But consider the variation
static buffer b(100);
int main()
{
buffer b(100);
do_something_with(b);
return 0;
}
Now, b is defined as a global-scope object. Attempts to wrap that definition in a try block
try // um, no, I don't think so
{
static buffer b;
}
catch (...)
{
abort();
}
int main()
{
do_something_with(b);
return 0;
}
won’t compile.
Each example exposes a fundamental flaw in buffer's design: Implementation details are exposed beyond buffer's interface. In this case, the exposed detail is the possibly failing new expression in buffer's constructor. That expression exists to initialize the private subobject buffer::p -- a subobject that main and other clients can't access and shouldn't even know exists. Certainly, those clients shouldn't have to fuss with exceptions spewed by such a suboject.
To improve the design integrity of buffer, we can catch the exception within the constructor:
#include <stdlib.h>
class buffer
{
public:
explicit buffer(size_t);
~buffer();
private:
char *p;
};
buffer::buffer(size_t const count)
: p(NULL)
{
try
{
p = new char[count];
}
catch (...)
{
abort();
}
}
buffer::~buffer()
{
delete[] p;
}
static void do_something_with(buffer &)
{
}
int main()
{
buffer b(100);
do_something_with(b);
return 0;
}
The exception is contained within the constructor. Clients, such as main, never knew the exception ever existed, and peace reigns once more.
Or does it? Notice that buffer::p doesn't change once it’s set. To prevent the pointer from being accidentally overwritten, a prudent designer would declare it const:
class buffer
{
public:
explicit buffer(size_t);
~buffer();
private:
char *const p;
};
Happy Happy Joy Joy, until:
buffer::buffer(size_t const count)
{
try
{
p = new char[count]; // ERROR
}
catch (...)
{
abort();
}
}
Once initialized, const members cannot be altered, even within their containing object's constructor body. const members can be set only within -- you guessed it -- a constructor’s member initializer list:
buffer::buffer(size_t const count)
: p(new char[count]) // OK
This puts us back at square one, recreating the very problem we originally tried to solve.
Hmm.
Okay, how about this: Instead of initializing p with a new expression, initialize it with a helper function that in turn uses new:
char *new_chars(size_t const count)
{
try
{
return new char[count];
}
catch (...)
{
abort();
}
}
buffer::buffer(int const count)
: p(new_chars(count))
{
try
{
p = new char[count]; // ERROR
}
catch (...)
{
abort();
}
}
This works, but at the cost of an extra function -- just to protect against an event that will almost never happen.
I find none of these proposals to be truly satisfactory. What I really want is a language-level solution to the partially constructed subobject problem -- one that does not induce the other problems described above. Fortunately, the language contains just such a solution.
Fairly late in their deliberations, the C++ Standard committee added so-called "function try blocks" to the language specification. Kissin' cousins of the try blocks we’ve come to know and love, function try blocks catch exceptions within entire function definitions, including member initializer lists. Unsurprisingly, because the language was not originally designed to support function try blocks, the syntax is a bit tortured:
buffer::buffer(size_t const count)
try
: p(new char[count])
{
}
catch
{
abort();
}
What looks like the usual {} after the keyword try actually demarcate the constructor function body. In effect, the {} serve double duty; otherwise, we'd be faced with the even more wretched
buffer::buffer(int const count)
try
: p(new char[count])
{
{
}
}
catch
{
abort();
}
(Note to the chronically bored: Even though the nested {} are redundant, this version will compile. In fact, you can nest as many {} as you want -- up to the limit of your compiler's patience.)
If we had multiple initializers in the initializer list, we'd have to put them all within the same function try block:
buffer::buffer()
try
: p(...), q(...), r(...)
{
// constructor body
}
catch (std::bad_alloc)
{
// ...
}
As with normal try blocks, we could also have any number of handlers:
buffer::buffer()
try
: p(...), q(...), r(...)
{
// constructor body
}
catch (std::bad_alloc)
{
// ...
}
catch (int)
{
// ...
}
catch (...)
{
// ...
}
Appalling syntax aside, function try blocks solve our original problem: All exceptions thrown by buffer subobject constructors stay corralled within buffer's constructor.
Because we now expect the buffer constructor to throw no exceptions, we should give it an exception specification:
explicit buffer(size_t) throw();
Come to think of it, we should be good little programmers and give all of our functions exception specifications:
class buffer
{
public:
explicit buffer(size_t) throw();
~buffer() throw();
// ...
};
// ...
static void do_something_with(buffer &) throw()
// ...
For our ongoing example, the final version is
#include <stdlib.h>
class buffer
{
public:
explicit buffer(size_t) throw();
~buffer() throw();
private:
char *const p;
};
buffer::buffer(size_t const count)
try
: p(new char[count])
{
}
catch (...)
{
abort();
}
buffer::~buffer()
{
delete[] p;
}
static void do_something_with(buffer &) throw()
{
}
int main()
{
buffer b(100);
do_something_with(b);
return 0;
}
Fire up Visual C++®, compile this example, sit back in smug comfort, and watch as the IDE boldly proclaims
syntax error : missing ';' before 'try'
syntax error : missing ';' before 'try'
'count' : undeclared identifier
'<Unknown>' : function-style initializer appears
to be a function definition
syntax error : missing ';' before 'catch'
syntax error : missing ';' before '{'
missing function header (old-style formal list?)
Oops.
Our stalwart compiler seems to have an Achilles heel. Sad to say, Visual C++ does not yet support function try blocks. Of the translators with which I typically test, only the Edison Design Group C++ Front End version 2.42 finds this code agreeable.
(By the way, I especially like how the compiler repeats the first error. Maybe it reckons you didn't believe it the first time.)
If you insist on using Visual C++, you can employ one of the earlier trial solutions in lieu of function try blocks. Of those, I would go with the extra new-encapsulating function. If you follow suit, consider making that function a template:
template <typename T>
T *new_array(size_t const count)
{
try
{
return new T[count];
}
catch (...)
{
abort();
}
}
// ...
buffer::buffer(size_t const count)
: p(new_array<char>(count))
{
}
This template is more generic than the original new_chars function, working for element types other than char. At the same time, it has stealth exception-related problems that I'll address in an upcoming column.
Robert Schmidt is a technical writer for MSDN. His other major writing distraction is the C/C++ Users Journal (http://www.cuh.com/), for which he is a contributing editor and columnist. In previous career incarnations he's been a radio DJ, wild-animal curator, astronomer, pool-hall operator, private investigator, newspaper carrier, and college tutor.