Exceptions - The Details

Exceptions - The Details

By Steve Cornish

Overload, 7(34):, October 1999


This is the second part of a three-part series of articles describing C++ exceptions. In this part we deal with:-

  • Special functions

  • Exception specifications

  • Function try blocks

  • std::nothrow

Special functions

The exception handling mechanism relies on two functions, terminate() and unexpected(), for coping with errors related to the exception handling mechanism itself.

void terminate()

The function std::terminate() is called:

  • If an exception is thrown but not caught.

  • If the exception handling mechanism finds that the stack is corrupted.

  • If a destructor propagates an exception during stack unwinding due to another exception (i.e. if a destructor throws while uncaught_exception() is true).

The function terminate() calls teminate_handler() which by default calls abort(). The behaviour of terminate() can be altered by supplying a function pointer to set_terminate().

void unexpected()

If a function throws an exception not allowed by its exception specification (see below) then:

  • the stack is unwound for the function

  • the function unexpected() is called.

The function unexpected() calls unexpected_handler() which by default calls terminate(). The behaviour of unexpected() can be altered by supplying a function pointer to set_unexpected().

bool uncaught_exception()

The function uncaught_exception() returns true from the point where the throw expression has been evaluated until the exception-declaration of a matching handler is constructed, or if the code enters unexpected(). uncaught_exception() has rather sinister uses which will be explained next month.

Exception specifications

Exception specifications are a way of specifying which exceptions a function may directly or indirectly throw in the function declaration. For example:

void f() throw (NoLatteException);
void (*fp)() throw (NoCookiesException);

If a virtual function has an exception-specification, then any function that overrides the virtual function shall only allow exceptions listed, or exceptions derived from those listed. This is called the co-variant rule.

struct X { .. };
struct Y { .. };

struct B {
   virtual void f() throw( X );
   virtual void g();
}

struct C : B {
   void f() throw ( X, Y );  // error
   void g() throw ( Y );     // fine
}

A function with no exception-specification is allowed to throw any exception. A function with the empty exception specification, throw(), is not allowed to throw any exceptions.

void f() throw();     // no exceptions

If an exception is thrown that is not allowed by a function's exception-specification, then unexpected() is called.

Exception specifications: to use, or not to use

Exception specifications are a way of specifying at the point of declaration, which exceptions may thrown by this function.

struct X { .. );
struct Y { .. };

struct Thing
{
    void a();                // 1
    void b() throw (X);      // 2
    void c() throw (X, Y);   // 3
    void d() throw();        // 4
}

A function that has no throw specification (such as on line 1), is allowed to propagate any exception.

The function on line 2 is only allowed to propagate exceptions of type X.

The function on line 3 is only allowed to propagate exceptions of type X and Y.

The function on line 4 is not allowed to propagate exceptions of any kind.

Note the careful use of the word propagate here; the functions may throw disqualified exceptions, but they are expected to catch them before they escape. So, what happens if a function throws an exception that its specification disallows? Does the program fail to compile in the first place, or get nasty.

The answer is, of course, the program gets nasty; it calls unexpected() .

Exception handling is a robust method for handling (distantly) reported errors. Consider the function:

void f( int ) throw( a, b, c );
Exception specifications look very useful on the surface, and they are definitely useful as self documentation.  But let's go back to why we have exceptions in C++…

If an exception that is not on the list is thrown, the default action is to terminate the program. That doesn't sound very robust. To correctly use exceptions specifications, you must include everything that can be thrown by any called function (direct or indirect). Since the primary usefulness of exceptions is to handle reported errors which may be far down the code calling chain, this means every function between the throw and the handler must specify the exception. This is pollution!

Exception specifications are hard to maintain, and carry serious (counterproductive) side-effects if not 100% correct.

Using throw() everywhere is another mistake people often fall into. Using this doesn't prevent exceptions being thrown, it merely tells the compiler that it shouldn't. If the function does, then it's terminate() time.

Taligent engineers use exception specifications only with an architect's approval.

Taligent's Guide to Designing Programs

And there's a further problem; exception specifications and templates don't mix.

At the JACC'99, Mike Ball of Sun Microsystems said "Every language contains a few experiments that didn't work out; finalize is one for Java." Well it looks like exception specifications could be one for C++.

Special uses

Function-try blocks

What is the difference between:

void f(int ii)
{
   try 
   {
       // whatever
   }
   catch (...)
   {
       // stuff
   }
}

and:

void f(int ii)
try
{
       // whatever
}
catch (...)
{
       // stuff
}

The answer is nothing, unless the function is a constructor or a destructor.

C::C(int ii, double id)
try
    : I(f(ii)), d(id)
{
    // rest of ctor
}
catch (...)
{
    // catches errors thrown 
    // from ctor body and 
    // member initialisation list
}

Now with the function-try-block, the member initialisation list is inside the try block. And for the destructor…

C::~C()
try
{
    // rest of dtor
}
catch (...)
{
    // catches errors thrown 
    // from dtor body
    // and member resources
    // released by class C.
}

The other style of try-catch would have exited the handler before the destructors for the members of C were called. Note that this means we can now promise and guarantee a destructor will not throw:

C::~C() throw()
try
{
    // stuff
}
catch (...)
{
    // stuff, if anything!
}

Since the function-try-block was a late addition to the C++ Standard, few compilers support this yet.

Also covered in :-

  • C++ Programming Language 14.4.6.1

  • C++ Primer 19.2.7

New - std::nothrow

The C++ standard has ratified a change to the new operator.

T *p = new T;

Previously, if the call to new above failed, a null pointer would've been returned. Under the ISO C++ Standard, an exception of type std::bad_alloc is thrown. It is possible to suppress this behaviour in favour of the old style by using the nothrow version.

T *p = new (std::nothrow) T;

A further interesting question is, if you don't have enough resources to allocate a request for memory, do you expect to have enough to be able to deal with it? Most operating systems will have slowed to be unusable long before the exception gets thrown.

In these extreme cases, exceptions give you the chance to perform a graceful shutdown even if you can't remedy the situation.

In the final article of this series I shall offer some best practice guidance for making use of C++ exceptions.






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.