★ wanayoo — archive 1999 http://www.msdn.microsoft.com/voices/deep.aspNouvelle recherche | Portail wanayoo
MSDN Online Voices   All Products  |   Support  |   Search  |   microsoft.com Home  
microsoft.com Home
  HOME  |   VOICES  |   LIBRARIES  |   COMMUNITY  |   DOWNLOADS  |   SITE GUIDE  |   SEARCH MSDN
Voices Archive 

by Robert Schmidt

Handling Exceptions, Part 10

Posted October 21, 1999     To be archived November 4, 1999

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.


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.

try Blocks

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.

Exposed Implementation

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.

const Member

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.

Function try Blocks

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()

// ...

Rounding Third And Heading For Home

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, 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.


Archived Deep C++

1999
October 7    Handling Exceptions, Part 9
September 7    Handling Exceptions, Part 8
August 19    Handling Exceptions, Part 7
August 5    Handling Exceptions, Part 6
July 15    Handling Exceptions in C and C++, Part 5
July 1    Handling Exceptions in C and C++, Part 4
June 17    Handling Exceptions in C and C++, Part 3
June 3    Handling Exceptions in C and C++, Part 2
May 10    Handling Exceptions in C and C++, Part 1


Photo Credit: Katie McCullough/Katie McCullough Photography

Deep C++ Glossary

ANSI

The American National Standards Institute technical committee, once designated X3J11 but now known simply as J11, is responsible for creating and maintaining the U.S. C programming language Standard. A similar committee, J16, is responsible for the collateral U.S. C++ Standard. Both committees represent the U.S. on the corresponding ISO committees.

ANSI C

The C language specified in ANSI's 1989 C Standard, and largely inherited from K&R C. Also known as C89 (from its year of adoption).

ANSI C Standard

Formal document name: ANSI X3.159-1989. The U.S. C language Standard published by ANSI in 1989, technically equivalent to -- and supplanted by -- the ISO C Standard published a year later.

The ARM

Acronym for The Annotated C++ Reference Manual, written by Margaret Ellis and Bjarne Stroustrup, and first published in 1991. The ARM is to Standard C++ as K&R is to Standard C: The de-facto standard of its day, and the foundation for the eventual ISO Standard.

automatic storage duration, automatic object

A C or C++ local-scope object explicitly declared auto or register, or not explicitly declared extern or static, has automatic storage duration. An object with such storage duration is an automatic object. Storage for these objects lasts until the block in which they are created exits. Automatic objects are what most programmers think of as "local variables" or "stack variables."

CV qualifiers

Standard-ese for the const and volatile type qualifiers.

D&E

Acronym for The Design and Evolution of C++, written by Bjarne Stroustrup, and first published in 1994. While the C and C9x Standards have corresponding Rationale documents, the C++ Standard does not. Instead the D&E serves as the de-facto Rationale for Standard C++.

dynamic storage duration, dynamically created/destroyed object

A C++ object is dynamically created via a new expression, and dynamically destroyed via a delete expression. Such objects have dynamic storage duration; their storage lasts until freed by operator delete or operator delete[].

EH

Shorthand notation for Standard C++ exception handling.

fully constructed

A C++ object is fully constructed if, and only if, its constructor has completed and its destructor has not begun. Full construction is aborted if the constructor throws an exception.

Since contained subobjects construct before the containing constructor begins, the property of full construction is recursive: If one subobject deep in a class hierarchy throws an exception, all nested containing objects up the chain fail to construct unless/until the exception is handled. Caveat Constructor.

ISO

Also known as the International Organization for Standardization. Contrary to popular belief, the name ISO is not an acronym, but derives from the Greek "isos" meaning "equal." (The English prefix "iso-" also derives from this same word.)

ISO committees JTC1/SC22/WG14 and WG21 are responsible for creating international C and C++ language Standards, respectively. The committees comprise representatives from national standards bodies; in the United States, those national bodies are ANSI committees J11 and J16.

ISO C Standard, a.k.a C Standard

Formal document name: ISO/IEC 9899:1990. There is also a corresponding Rationale.

This document is the international C language Standard published by ISO in 1990. It is technically identical to the ANSI C Standard; however, the two published Standards use slightly different nomenclature and section numbering.

Since 1990, ISO has updated the C Standard with three addenda:

ISO/IEC 9899 AM1. 1995 Amendment 1, adding international character support. Also known as Normative Addendum 1.

ISO/IEC 9899 TCOR1. 1995 Technical Corrigendum 1, correcting technical errors in the Standard.

ISO/IEC 9899 TCOR2. 1996 Technical Corrigendum 2, correcting a smaller number of additional technical errors.

The C Standard is not free, nor can you purchase it from ISO. You must instead purchase it from either your nation's ISO member bodyor a reseller.

ISO C9x Standard (Final Committee Draft)

Formal document name: WG14/N843. There is also a corresponding Rationale.

ISO working group JTC1/SC22/WG14 is currently revising the entire C Standard. The C language specified by this revised Standard is colloquially called C9x. As the name suggests, C9x standardization is scheduled for completion in the 1990's.

ISO C++ Standard, a.k.a. C++ Standard

Formal document name: ISO/IEC 14882:1998. This document is the international C++ language Standard published by ISO in 1998.

As with the C Standard, you must purchase the C++ Standard. Fortunately the 1997 Final Committee Draft, which is freely available online, is mostly identical to the actual Standard. If you decide to purchase the Standard, I recommend you save money and trees: the paper version is ten times the cost of the electronic version.

K&R

Shorthand for Brian Kernighan and Dennis Ritchie's book The C Programming Language, first edition. Published in 1978, K&R formed the basis of the ANSI C Standard introduced a decade later.

K&R C

The C language specified in K&R. K&R C lacks several key features of Standard C: function prototypes, const and volatile keywords, void type, enumeration types, and a well-defined library.

lvalue

Literally an "l value" or "left value." So-called because, in K&R C, an lvalue can appear on the left side of an assignment expression. In Standard C or C++, an lvalue is more properly a "locator value" designating an object.

Lvalues are either modifiable or non-modifiable, a concept generally mapping to non-const and const objects, respectively. Their names aside, non-modifiable (const) lvalues cannot appear on the left in an assignment. (Because K&R C does not have the const keyword, all K&R lvalues are modifiable.)

name decoration or name mangling

Encoding of C++ names into C pseudo-names discernible by C linkers. In particular, such mangling allows C linkers to support class/namespace scope and function overloading, by turning what would be invalid redefinition of the same name into a new definition of a unique synthetic name.

The C++ Standard does not regulate the algorithm mapping between original C++ names and linker-friendly mangled names. Instead, each translator vendor is free to create a unique naming scheme. This suggests that object files with different name-mangling schemes cannot be mixed.

(Note that other considerations -- parameter-passing method, register allocation, stack alignment -- also prevent inter-vendor object file mixing. Name mangling just takes a bad interoperability situation and makes it worse.)

partially constructed

A C++ object is partially constructed if, and only if, its constructor has not finished execution.

RTTI

Literally "Run-Time Type Identification," the Standard C++ mechanism for class objects to identify their dynamic or run-time types. RTTI is supported by the keywords dynamic_cast and typeid and the Standard Library header <typeinfo>. An object's run-time identity is typically stored as data accessed through the object's v-table.

rvalue

Literally an "r value" or "right value." Rvalues always appear on the right side of an assignment statement. Unlike lvalues, which designate objects, rvalues designate values only -- at least in C. C++ also has "class rvalues" designating unnamed temporary objects of a constructed (class) type; such objects are conceptually values of that type.

Standard C

The C language specified in the ISO C Standard. Technically identical to ANSI C.

Standard C++

The C++ language specified in the ISO C++ Standard.

storage-class specifier

In Standard C, any of the keywords

In Standard C++, the above keywords plus

As the term suggests, a storage class specifier generally describes what kind of storage objects occupy, how long that storage exists, and how visible the storage is. The anomaly is mutable, which really doesn't describe storage at all. However, since mutable can appear in exactly the same grammatical contexts as the other "real" storage class specifiers, considering mutable a storage class specifier does simplify the C++ grammar.

subobject

A C++ object contained by other objects. A subobject is a named data member object, an unnamed base class object, or an array element.

Ant. complete object.

type qualifiers

To the CV qualifiers const and volatile, C9X adds a third: restrict. Rather than call the resulting set of attributes "CRV qualifiers" or some such, the C9X Standard committee elects to call them simply "type qualifiers."



Back to topBack to top

Did you find this material useful? Gripes? Compliments? Suggestions for other articles? Write us!

© 1999 Microsoft Corporation. All rights reserved. Terms of use.