Borland Online And The Cobb Group Present:


March, 1996 - Vol. 3 No. 3

C++ Language basics - Exception handling: Understanding dynamic scope and stack unwinding

by John M. Dlugosz

In this article, we'll take a look at the basics of exception handling and some of the underlying concepts it embodies. This article isn't a treatment of error-handling techniques; instead, we'll explain what the try/throw/catch construct does on a fundamental level. After reading this article, you should have a full understanding of the semantics of using this exotic control structure and of the basic implementation syntax.

The syntax of try/catch is fairly simple. The semantics involve two concepts new to C++: dynamic scope and stack unwinding. We'll look at both concepts in more detail later­­but first, let's take a look at the syntax.

Syntax

A try/catch control structure is written

try {
   statements...
   }
catch (parameter) { handler... }
catch (parameter) { handler... }

The first part of the structure is called a try block. You can put any statements inside the block controlled by the try keyword, and those statements will be protected. Normal scope rules apply with respect to the use of curly braces, so any variables you define within the try block are local to that block.

One or more error handlers follow the try block, using syntax that looks similar to a function definition. A parameter list with a single parameter follows the keyword catch. After the parameter list comes a block containing arbitrary code.

Meanwhile, you use a throw expression to initiate an exception. The throw keyword takes an argument, which is matched with the parameter of the proper handler, as we'll explain in the next section. Listing A contains a complete program that illustrates this syntax.


Listing A: An example of the try/catch control structure

#include <iostream.h>
#include <stdlib.h>

int divide (int top, int bottom)
{
if (bottom==0)  throw "divide by zero error!";
return top/bottom;
}

int main (int argc, char* argv[])
{
int a= atoi(argv[1]);  //first command-line parameter
int b= atoi(argv[2]);  //second command-line parameter
cout << "attempting to divide " << a << " by " << b << endl;
try {
   int result= divide (a,b);
   cout << "The result is: " << result << endl;
   }
catch (const char* message) {
   cout << "error detected. Message is: "
        << message << endl;
   }
cout << "program done.\n";
return 1;
}

Dynamic scope

Normally, C++ uses what's known in computer science circles as lexical scope. In other words, the contextual meanings of variables and other symbols are matched to their definitions based on rules involving the text of the source code. Symbols are bound at compile-time. For example, if you have several variables called x in a program, it's simple to tell which x you mean at any point. When the program uses x, the compiler first looks for definitions local to the block in which x is used. It then looks outward to enclosing blocks within the function, then at class scope, and finally for global variables. The compiler matches the usage of a symbol with that symbol's definition based on a static analysis of the source code­­in short, on where the symbol is written in the program's text.

This idea of lexical scope is so ingrained in C++ and other languages, such as Pascal and Ada, that any other kind of scope can seem alien to developers. Naturally, if you hear the concept of dynamic scope explained only in computer-science terminology, it can be baffling. In some languages, such as Lisp and Perl, dynamic scoping is part of the design. But you can also find the concept in C++, by analogy with other programming.

The ANSI/ISO C++ specification says, "When an exception is thrown, control is transferred to the nearest handler with an appropriate type; 'nearest' means the handler whose try block was most recently entered by the thread of control and not yet exited...." So, the very definition of C++ states that the binding of throws to catches is temporal, not lexical. That is, it depends not on where the binding is written in the program, but on the flow of execution within the program up to that point.

To understand this concept, don't look for other name-binding rules in C++. Instead, look at the runtime sequence of events. The program in Listing B, illustrates the idea of temporal binding. A function will read the value of context as it was last set. So, foo() will read context as different values at different times, based on where context is called from and how execution reached that point.


Listing B: An example of temporal binding

#include <iostream.h>

class context {
   static const context* current;
   const context* const previous;
   const char* name;
public:
   context (const char* name);
   ~context();
   static void print();
   };

context::context (const char* name)
: name(name), previous(current)
{
current= this;
}

context::~context()
{
current= previous;
}

void context::print()
{
cout << "Context is: " << current->name << endl;
}

const context* context::current= 0;

////////////////////////////////////////////

void foo()
{
cout << "(foo) ";
context::print();
}

void baz()
{
context x ("local to baz");
foo();
// x goes out of scope here.
}


void bar()
{
foo();  //call foo, which is context-aware
context x ("local to bar");
foo();  //same function foo called; knows the new context.
baz();
// x goes out of scope here, and old context is restored.
}


int main()
{
context x ("main");
foo();
bar();
baz();
return 0;
}

   
/*  output is:
(foo) Context is: main
(foo) Context is: main
(foo) Context is: local to bar
(foo) Context is: local to baz
(foo) Context is: local to baz
*/


The creation and destruction of context variables mimics the way try blocks work. When a program enters a try block during execution, the program remembers the block in a stack. When the program exits the try block, the block is popped from the stack. To search for a handler when your program throws an exception, the program searches the items on the stack from nearest to farthest (using the ANSI specification's definition of "nearest").

Stack unwinding

The other important principle to understand is stack unwinding. Something very powerful happens when a program throws an exception­­it's more than C's longjump() could accomplish. The general idea hinges on one fundamental concept: A program will call destructors for local variables upon leaving a block. You've seen this happen when control returns from a function or simply flows out of a local block. But the fundamental concept applies to any manner of leaving the block, as you can see in Figure A.


Figure A - This code illustrates how a program calls destructors for local variables.

void foo ()
{
if (test()) {
   SomeClass test_object;
   if (a==b)  return;  //test_object is destructed.
   if (a==c)  goto exit:  //test_object is destructed.
   if (a==d)  throw 42;   //test_object is destructed.
   bar();  //do some meaningless work...
   } // test_object is destructed as control
     // flows off the end of block.
exit: bar();
}

No matter how control leaves the scope of test_object, the program will call the variable's destructor. When the C++ standardization committee introduced exceptions to C++, it introduced the concept of "unwinding" to do the same thing. Now an exception can exit several scopes in one fell swoop, but so can a goto (or its cousins, break and continue). Figure B shows an example.


Figure B - The goto exits three blocks and calls destructors for three objects.

    {
   SomeClass object1;
   if (whatever()) {
      SomeClass object2;
      for (int loop= 0;  loop < max;  loop++) {
         SomeClass object3;
         if (whoops())  goto panic;
      }
   }
}
panic:  do_something();

The goto will exit three local blocks and cause the program to call the destructors of three objects, each in a different scope. An exception is no different­­it exits one or more scopes and causes the program to call destructors. The real difference is that you don't know where the exception will end up until runtime. But even so, the exception must return to its previous location­­an outer level or calling function.

We've presented only the general idea. Explaining the specifications of stack unwinding can be quite complex­­we need to deal with the loose ends while explaining the concept in a more abstract way, since not every scope is formed by blocks in this manner.


Figure C - Our exception program yields this output.

entering foo.
Object B constructed at 0x0012ff70	Note 1
Object B constructed at 0x0012ff24	Note 2
Object B constructed at 0x0012ff28	Note 2
Object B constructed at 0x0012ff2c	Note 2
in the body of C's constructor with parameters (4,5,6)
I constructed an object of type C named mort.	Note 3
destructing an object of type C
Destructed a B object at 0x0012ff2c	Note 4
Destructed a B object at 0x0012ff28	Note 4
Destructed a B object at 0x0012ff24	Note 4
Destructed a B object at 0x0012ff70	Note 5
entering foo.				Note 6
Object B constructed at 0x0012ff70	Note 7
Object B constructed at 0x0012ff24	Note 8
Destructed a B object at 0x0012ff24	Note 9
Destructed a B object at 0x0012ff70	Note 10
caught an error: B's constructor fails	Note 11

An exception: play-by-play

Often the best way to understand something is to see it in action. Listing C provides a detailed dissection of an exception taking place. Figure C shows the program's output; it includes references to several notes in Table A.

Table A: Program output notes
Note 1 The constructor for the local variable in foo.
Note 2 The members of mort being constructed in bar.
Note 3 The function bar is about to return, so the program will call mort's destructor.
Note 4 The destructors for mort's members.
Note 5 The destructor for the local variable in foo.
Note 6 Normally, functions call functions, and the program calls constructors for local variables and objects nested within them. On exit, the program calls context destructors. Now we run through the process again, but this time an error occurs.
Note 7 The constructor for the local variable in foo.
Note 8 The constructor for the first member (sub_1) of mort in bar. Next, the program calls the constructor for the second member (sub_2) of mort. However, an error arises from these lines:
 
  if (x < 0)  //a pre-condition check
     throw simple_exception ("B's constructor fails");
  cout << "Object B constructed at " << (void*)this << endl;
The program doesn't print Object B constructed at... because it detects the error first. The throw statement aborts the constructor, and execution never reaches the cout line.
Note 9 This is stack unwinding in action. The program destroys the first member of mort (sub_1). This undoes the partial construction of mort; the program can't destroy mort, since it wasn't fully constructed. The program will destroy only parts that it successfully constructed. So, the program tears down sub_1, but not sub_2 and sub_3.
Note 10 The program calls the destructor for the local variable in foo, because the program declared the variable inside the try block, and the try block is now exiting. However, the stack unwinding doesn't stop at the associated catch handler. It isn't of the proper type (it wants an int), so the program passes it over. The search continues outward, back the way it came.
Note 11 The program finds the catch handler in main. The parameter is suitable, so the program uses this catch block. The object thrown is a derived class (simple_exception) to the base class mentioned in the catch parameter list (exception_base). But the catch parameter is a reference, so the program can control the actual thrown object polymorphically. Note that message() works as a virtual function. This alludes to a future topic: how objects are transported from the throw expression to the catch point.

Conclusion

Understanding exception handling means understanding exactly what the try/throw/catch construct does. Dynamic scope can provide for flexible error handling by letting you write a function without explicit control over how the program handles its errors, thus letting the caller decide how to deal with errors. Given the basic principles of C++, once we introduce dynamic scope, understanding the concept of stack unwinding becomes essential. In a future article we'll discuss how code can fail to be "exception safe."

John M. Dlugosz is a C++ consulting expert, developer, and prolific writer. He's a member of Borland's TeamB, he leads the C++ Study Group on CompuServe, and he's the author of C++: A Guru's Handbook. You can reach John via CompuServe at 70007,4657, on the Internet at jdlugosz@dlugosz.com, or on the Web at http://www.dlugosz.com/~jdlugosz/.

Return to the Borland C++ Developer's Journal index

Subscribe to the Borland C++ Developer's Journal


Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.