CSCI 431 Lecture Notes - Generic Types and Exception Handling in Ada

But first, continuing last week's lecture: A C example of subprograms as parameters (courtesy of Dean Brock)

The Unix quick sort routine, qsort, is probably the most frequently used C routine that takes a function as an argument. Here's a man page entry for qsort that explains how it uses its four arguments:


void qsort(void *base,
           size_t nmemb,
           size_t size,
           int (*compar) (const void *, const void *));

base Points to the first entry in the table.

nmemb Specifies the number of entries in the table.

size Specifies the size in bytes of each table entry.

compar Points to the user-specified function to be used to compare pairs of table elements. The comparison function will be called with two parameters that point to the two elements to be compared. The comparison function must return an integer less than, equal to, or greater than zero, depending on whether the first element in the comparison is considered less than, equal to, or greater than the second element.

Here's a sample use of the qsort routine. ether_table.ei is an array of ether_info structures. These ether_info structures contain a six-byte Ethernet address in a field ei_eaddr. First, we need a function that can compare Ethernet addresses stored in two ether_info structures. The function, ether_comper, will be passed pointer to the structures and return –1, 0, or 1 depending on the result of its comparison.


   int ether_comper(void *p_ei1, void *p_ei2)
   {
      int j;
      for (j=0; j<6; ++j) {
         if (((struct ether_info *)p_ei1)->ei_eaddr.ether_addr_octet[j]
        	< ((struct ether_info *)p_ei2)->ei_eaddr.ether_addr_octet[j])
            return(-1);
         if (((struct ether_info *)p_ei1)->ei_eaddr.ether_addr_octet[j]
        	> ((struct ether_info *)p_ei2)->ei_eaddr.ether_addr_octet[j])
            return( 1);
      } 
      return(0);
   }

The function, ether_compar, is passed into the qsort routine, along with the base of the array, the number of elements in the array, and the size of those elements. On return from qsort, the array is sorted.


   qsort((char *) ether_table.ei, ETHER_TABLE_SIZE,
         sizeof(struct ether_info), ether_comper)

Generics in Ada (taken from the Lovelace tutorials by David A. Wheeler)

One of Ada's strongest claims is the ability to code for reuse. C++ also claims reuse as one of its goals through Object Oriented Programming. Ada-83 allowed you to manage the data encapsulation and layering through the package mechanism and Ada-95 does include proper facilities for OO Programming. Where Ada led however, and C++ is following is the area of generic, or template programming.

It's often useful to first create a more generic version of a subprogram or package and then use that generic version to create more specific subprograms or packages. Ada's capability to do this is called a generic, and it's the same thing as C++'s templates. Generics are also somewhat similar to C's "#define" preprocessor command, though Ada generics are type-checked and thus much safer.

It's probably easiest to understand this by example. First, let's write a procedure to swap two Integers:

  -- Here's the declaration (specification):
  procedure Swap(Left, Right : in out Integer);

  -- .. and here's the body:
  procedure Swap(Left, Right : in out Integer) is
    Temporary : Integer;
  begin
    Temporary := Left;
    Left := Right;
    Right := Temporary;
  end Swap;

Swap is a perfectly fine procedure, but it's too specialized. We can't use Swap to swap Floats, or Characters, or anything else. What we want is a more generic version of Swap, but one where we can substitute the type "Integer" with a more generic type. A generic version of Swap would look like this:

  -- Here's the declaration (specification):
  generic
    type Element_Type is private;
  procedure Generic_Swap(Left, Right : in out Element_Type);

  -- .. and here's the body:
  procedure Generic_Swap(Left, Right : in out Element_Type) is
    Temporary : Element_Type;
  begin
    Temporary := Left;
    Left := Right;
    Right := Temporary;
  end Generic_Swap;

In general, to create a generic version of a subprogram (or package), write the subprogram using a few generically-named types. Then precede the subprogram or package with the keyword ``generic'' and a list of the information you'd like to make generic. This list is called the generic formal parameters; this list is like the list of parameters in a procedure declaration.

To use a generic subprogram (or package), we have to create a real subprogram (or package) from the generic version. This process is called instantiating, and the result is called an instantiation or instance. For example, here's how to create three Swap procedure instances from the generic one:

  procedure Swap is new Generic_Swap(Integer);
  procedure Swap is new Generic_Swap(Float);
  procedure Swap is new Generic_Swap(Character);

Note that when you instantiate a generic, you ''pass'' types in the same way that you pass parameters to an ordinary procedure call.

From here on, you can call the Swap procedure that takes Integers, the Swap procedure that takes Floats, and the Swap procedure that takes Characters. Thus if A and B are both of type Integer, Swap(A,B) would swap their values. As for any other Ada subprogram, if different subprograms share the same name, Ada will determine at compile time which one to call based on the argument types.

Here's a simple test program for Generic_Swap:

with Generic_Swap;

procedure Tswap is
 procedure Swap is new Generic_Swap(Integer);
 A, B : Integer;
begin
 A := 5;
 B := 7;
 Swap(A, B);
 -- Now A=7 and B=5.
end Tswap;

For brevity the procedure Swap is being instantiated in procedure Tswap, but in most real programs almost everything is enclosed in a package and the instantiations would be inside the package body. So let's look an example of a generic package. This example of a generic package is straight from the Ada 95 RM section 12.8.

Let's imagine that you want to create a generic package for a Stack that takes operations Push and Pop. Here's one way to define such a Stack; we'll define a Stack package that stores some Item type and has a maximum size:

  generic
    Size : Positive;
    type Item is private;
  package Generic_Stack is
    procedure Push(E : in  Item);
    procedure Pop (E : out Item);
    Overflow, Underflow : exception;
  end Generic_Stack;

Now a definition needs to be implemented, so here's a sample implementation:

  package body Generic_Stack is
    type Table is array (Positive range <>) of Item;
    Space : Table(1 .. Size);
    Index : Natural := 0;

    procedure Push(E : in Item) is
    begin
      if Index >= Size then
        raise Overflow;
      end if;
      Index := Index + 1;
      Space(Index) := E;
    end Push;

    procedure Pop(E : out Item) is
    begin
      if Index = 0 then
        raise Underflow;
      end if;
      E := Space(Index);
      Index := Index - 1;
    end Pop;

  end Generic_Stack;

Somewhere else you can instantiate the Generic_Stack package. If you wanted to instantiate a new package called ``Stack_Int'' which could hold 200 Integers, you could say:

  package Stack_Int is new Generic_Stack(Size => 200, Item => Integer);

The ''Size =>'' and ''Item =>'' are optional; you could omit them if you wanted to, but including them makes the code clearer. From then on, you could "Push" a new Integer onto Stack_Int by saying:

  Stack_Int.Push(7);

Exception Handling in Ada (taken from the Lovelace tutorials by David A. Wheeler)

The Basics

Errors and other exceptional situations must be handled by programs that work in the real world. Ada provides facilities to deal with these real problems which make handling them much easier. In Ada, an exception represents a kind of exceptional situation, usually a serious error. At run-time an exception can be raised, which calls attention to the fact that an exceptional situation has occurred.

The default action when an exception is raised is to halt the program. Usually the program will print out the name of the exception and where the problem took place, though this depends on your compiler. The next few sections will show how to override this default.

If you don't want to halt the program, you'll need to tell Ada what to do instead by defining an exception handler. An exception handler states what exceptions to handle and what to do when a given exception is raised.

Exceptions generally represent something unusual and not normally expected - reserve their use for things like serious error conditions. They shouldn't be used for ''expected'' situations, because they can be slower and if incorrectly handled can stop a program. The place where an exception is raised may be far away from where it is handled, and that makes programs with a voluminous number of different exceptions harder to understand. Instead, exceptions should be used when a subprogram cannot perform its job for some significant reason.

Ada has a number of predefined exceptions that are raised when certain language-defined checks fail. The predefined check you're most likely to see is Constraint_Error; this exception is raised when a value goes out-of-bounds for its type. Examples of this include trying to store a value that's too large or too small into that type, dividing by zero, or using an invalid array index.

Naturally, there is some run-time overhead in performing all these checks, though less than you might think. It is possible to suppress these language-defined checks; this should only be done after the program is thoroughly debugged, and many people think that it shouldn't be done even then.

Some packages define their own exceptions, for example, Text_IO defines the exception End_Error that is raised when you attempt to ''Get'' something after you've reached the end of the file, and Name_Error is raised if try to open a file that doesn't exist.

Predefined Exceptions

Ada includes five built-in exceptions (maybe more):

User Defined Exceptions: Declaring Exceptions

Before you can raise or handle a user defined exception, it must be declared. Declaring exceptions is just like declaring a variable of type exception; here's an example:

  Singularity : exception;

To be complete, here's the syntax for defining an exception, describing using BNF:

  exception_declaration ::= defining_identifier_list ": exception;"

Exception declarations are generally placed in a package declaration.

Raising an exception is easy, too - just use the raise statement. A raise statement is simply the keyword "raise" followed by the name of the exception. For example, to raise the "Singularity" exception defined above, just say:

  raise Singularity;

The syntax in BNF is:

  raise_statement ::= "raise" [ exception_name ] ";"

You'll notice that the exception_name is optional; we'll discuss what that means in the next section.

Handling Exceptions

As we've noted many times, if an exception is raised and isn't handled the program stops. To handle exceptions you must define, reasonably enough, an exception handler.

When an exception is raised (by the raise statement) Ada will abandon what it was doing and look for a matching exception handler in the sequence of statements where the exception was raised. A sequence of statements is the set of statements between the keywords "begin" and "end". If Ada doesn't find a match, it returns from the current subprogram (cleaning up along the way) and tries to find a matching exception handler in the caller (from where it was called). If it doesn't find one there, it exits that subprogram (cleaning up along the way) and tries yet again. Ada keeps repeating this process until it finds a matching exception handler or until it exits the program.

An exception handler is defined just before the "end" statement that matches a "begin" statement.

For example, here's a procedure that "Open"s a file if it can, and if the file doesn't exist it "Create"s it.

  procedure Open_Or_Create(File : in out File_Type;
                           Mode : File_Mode; Name : String) is
  begin
    -- Try to open the file. This will raise Name_Error if
    -- it doesn't exist.
    Open(File, Mode, Name);
  exception
    when Name_Error =>
      Create(File, Mode, Name);
  end Open_Or_Create;

Here's a simplified BNF of an exception handler:

  exception_handler ::= exception_choice { "|"  exception_choice } "=>"
                        sequence_of_statements
  exception_choice  ::= exception_name | "others"

The keyword "others" means all exceptions not explicitly listed in this exception handler; thus, you can handle all exceptions if you want to.

Inside an exception handler you can do any kind of processing you wish. If, after processing, you find you need to raise the same exception to a higher level, you can use the "raise" statement without the name of an exception. A raise statement without a named exception re-raises the exception being handled. Raise statements can only re-raise exceptions inside an exception handler.

Comparison to C++ (taken from the C/C++ to Ada tutorial by Simon Johnston)

Unlike C++ where an exception is identified by its type in Ada they are uniquely identified by name. To define an exception for use, simply

parameter_out_of_range : Exception;
These look and feel like constants, you cannot assign to them etc, you can only raise an exception and handle an exception.

In C++ there is no exception type, when you raise an exception you pass out any sort of type, and selection of the exception is done on its type. In Ada as seen above there is a 'psuedo-type' for exceptions and they are then selected by name.

Firstly lets see how you catch an exception, the code below shows the basic structure used to protect statement1, and execute statement2 on detection of the specified exception.

try {
  statement1
} catch (declaration type) {
  statement2
}

begin
  statement1
exception
  when ident => statement2
  when others => statement2
end;
Let us now consider an example, we will call a function which we know may raise a particular exception, but it may raise some we don't know about, so we must pass anything else back up to whoever called us.
try {
  function_call();
} catch (const char* string_exception) {
  if (!strcmp(string_exception, "the_one_we_want")) {
    handle_it();
  } else {
    throw;
  }
} catch (...) {
  throw;
}

begin
  function_call;
exception
  when the_one_we_want => handle_it;
  when others          => raise;
end;
This shows how much safer the Ada version is, we know exactly what we are waiting for and can immediately process it. In the C++ case all we know is that an exception of type 'const char*' has been raised, we must then check it still further before we can handle it.

You will also notice the similarity between the Ada exception catching code and the Ada case statement, this also extends to the fact that the when statement can catch multiple exceptions. Ranges of exceptions are not possible, however you can or exceptions, to get:

begin
  function_call;
exception
  when the_one_we_want |
       another_possibility => handle_it;
  when others              => raise;
end;
This also shows the basic form for raising an exception, the throw statement in C++ and the raise statement in Ada. Both normally raise a given exception, but both when invoked with no exception re-raise the last one. To raise the exception above consider:
throw (const char*)"the_one_we_want";

raise the_one_we_want;