Hans Konrad Buhrer
konib@hotpop.com
Using the "Ada
Tasking in C++" Library
C++
Language Restrictions
Compatibility
The Header File "Task.h"
The
Enumeration Priority
The
Abstract Class Task
Using Task Utility Functions
Defining Specific Task Classes
Defining Members of Specific Task
Classes
The Global Task-Related Utility
Functions
The Header File
"Synchronization.h"
The
Abstract Class Any_Entry
Entry Classes
The Entry Class Template Entry
The Entry Class Entry0
The Entry Class Template Entry1
The Entry Class Template Entry2
The Entry Class Template Entry3
The Entry Class Template Entry4
The Entry Class MsgEntry
The Entry Class Template SignalEntry
The Class Rendezvous
Creating Rendezvous Objects
Selecting a Rendezvous Kind
Using Rendezvous Member Functions
The Header File
"Exception.h"
The Class Exception
Runtime Error Exception Types
Runtime Error Exception Attributes
Throwing Runtime Error Exceptions
Escape Exception Types
Catching ALL
Exceptions
It is sometimes called concurrency, multi-threaded code, parallel programming, or
multi-tasking.
But no matter what you call it, one thing is sure: C++ does not directly support
it. While C++ allows multiple concurrent threads within an application program,
it doesn't provide any facilities to support parallel programming. And that is
- considering the wide popularity of C++ - somewhat regrettable.
Of course, most modern operating systems do support parallel programming in one way or another: For example, Windows has threads, Unix has pthreads, and VMS has processes. However, the means by which operating systems support synchronization and communication between concurrent threads is rather primitive. Shared memory, mutexes, asynchronous system traps, and event flags come to mind. Using such low-level features in an application program is notoriously error-prone, obscures the program logic, and is 100% non-portable.
And then there is the DoD programming language Ada.
Ada has built-in support for concurrent threads (called tasks) and built-in support for task synchronization and communication (through entry and rendezvous). The Ada parallel programming paradigm is extremely powerful and versatile. I will call the Ada parallel programming paradigm Ada tasking in this document, though it may have other more descriptive names.
Well, Ada as a programming language had only limited success - understandably. Ada is very complex, hard to use, and has a thick, cryptic language reference manual that is subject to ongoing interpretation. Some people have called the Ada language project a "boondoggle" and the Ada tasking paradigm "hilarious". While the former could be argued the latter is most definitely not true. Anybody who knows Ada tasking and has actually used it in a program to implement multiple concurrent threads would probably call it "dynamite". The power of the Ada tasking paradigm and its intuitive, simple usage model is quite remarkable.
Anyway, the Ada tasking paradigm can easily be implemented in C++. An Ada task is implemented in C++ as an object of a task class. And an Ada rendezvous is represented in C++ by a rendezvous object within a local block. The "Ada tasking in C++" library provides all the header files and object code needed to use the Ada tasking paradigm in a C++ program. Even the syntactic appearance of the Ada tasking constructs in C++ is somewhat reminiscent of their Ada originals.
In the following paragraphs I will try to give a brief overview of the Ada tasking paradigm, using C++ terms.
Ada tasking is based on three simple concepts: Task, Entry, and Rendezvous:
Each task belongs to a task class. The tasks of each task class share the same code, but they are executed independently and thus have different execution states. An application program may define many different task classes. All task objects of all task classes are executed in parallel - subject to priority rules and eligibility. All application-defined task classes (called specific task classes) are derived from the predefined class Task.
Each task class has entry objects as non-static, public members. All tasks of a task class thus have the same interface, but of course their entries are in different states. A task (the caller task) may synchronize and exchange data with some other task (the owner task) by calling an entry of the owner task. Each entry belongs to a predefined entry class or to an instance of a predefined entry class template.
A rendezvous is implemented by a rendezvous object declared within a local block (called rendezvous block). All rendezvous objects belong to the predefined class Rendezvous. The rendezvous begins when the rendezvous object is constructed and ends when the rendezvous object is destroyed, normally at the end of the rendezvous block. The caller task remains suspended until the rendezvous ends.
Calling entries is the prescribed means by which tasks synchronize and communicate with each other. A task calls entries of another task like member functions. When a task makes an entry call it is suspended at the point of the call until the owner task has accepted the entry call. Entries may have parameters and a task passes actual parameter values as it makes an entry call. During the rendezvous - through the rendezvous object - the owner task has access to the entry parameters passed by the caller task.
It is possible for an entry to be called several times before the owner task can accept any calls. Such entry calls - with associated actual parameters - are queued by the entry and all caller tasks are suspended. The owner task will eventually accept the calls in the order in which they were received.
A task may only accept entry calls from - and perform a rendezvous through - its own entries. At the start of a rendezvous, the owner task selects the set of entries (the rendezvous entries) from which to accept a call. But only one entry call (or none if the rendezvous fails) is actually accepted in each rendezvous. If no call is pending on any of the rendezvous entries, the owner task may wait until a task makes an entry call. If several of the rendezvous entries have pending calls, the owner task accepts one of them arbitrarily.
Task interaction is strictly asymmetric. A task that provides services typically has entries and only performs rendezvous; a task that processes data typically only makes entry calls. However, rendezvous and entry calls are not mutually exclusive. A task may have entries (and thus perform rendezvous) and also make entry calls to other tasks.
The interface of the "Ada Tasking in C++" library is implemented by three
separate header files: "Task.h", "Synchronization.h", and
"Exception.h". The header files are typically located in a subdirectory named
"atask" below a standard include directory.
The C++ source files of an application program may freely include these header files, for
example:
#include <atask/Task.h>
#include <atask/Synchronization.h>
#include <atask/Exception.h>
Other header files distributed with the "Ada Tasking in C++" library should not be used by an application program. They are non-portable and may change from release to release without warning. If your application program requires functionality beyond what is available through "Task.h", "Synchronization.h", and "Exception.h" please contact the author.
The code of the "Ada Tasking in C++" library is distributed as a static library named "atask.lib" or "atask.a". It is not currently available as a DLL or a shared library.
The "Ada Tasking in C++" library imposes a few restrictions on how to use the C++ programming language and environment in an application program. The restrictions are minor, but the programmer has to remember and obey them.
The "Ada Tasking in C++" library is based on the
Win32 API. It is fully compatible with any code that uses the Win32 API. In particular,
threads created through the "Ada Tasking in C++" library (task threads)
can freely interact with other threads. Task threads are free to use whatever Win32
interfaces they desire, for example event flags, mutexes, or WaitFor...
functions.
Compatibility between the "Ada Tasking in C++" library and MFC is uncertain. I
don't see any reason why task threads should not be compatible with CWinThread objects, but I have not performed any tests.
Unknown.
The header file "Task.h" defines the class Task and related types, as well as global task-related utility functions. An application program relies on the declarations in "Task.h" to define specific task classes.
The enumeration Priority represents a task's priority.
enum Priority {
idle_Priority,
lowest_Priority,
low_Priority,
normal_Priority,
high_Priority,
highest_Priority,
time_critical_Priority
};
Every task has a priority. The priority of a task is a measure of its urgency or precedence relative to other tasks. At any given time, the task with the highest priority is always the task executed by the processor. If several tasks have the same, highest priority, they are all executed in a time-sliced manner.
The priority of a task may change over time. Therefore, the priority specified in the definition of a task class is often called the base-priority of the tasks of that class. A task's actual priority never drops below its base-priority, but it may rise above it. For example:
The priority time_critical_Priority is reserved for tasks that perform brief, time-critical operations.
The class Task is the base class of all specific task classes an application program may define (see Defining Specific Task Classes).
class Task
{
public:
bool activate();
void terminate();
bool has_activated();
bool has_terminated();
bool is_active();
Priority get_priority();
size_t get_size();
Task();
~Task();
protected:
virtual void body() = 0;
virtual Priority priority();
virtual DWORD stack_size();
virtual size_t virtual_sizeof() = 0;
};
In an application program, different types of concurrent threads are implemented as different specializations of the class Task. Task objects (of all task classes) are executed by the processor in parallel. A task object can synchronize and exchange data with another task object by calling its entries or by performing a rendezvous (see "Synchronization.h").
Every task object goes through a life cycle, as implemented by its member functions. A task object is
Construction and activation often occur simultaneously, because many task constructors invoke this->activate(). Similarly, destruction and termination often occur simultaneously, because many task destructors invoke this->terminate().
The class Task provides two groups of member functions. Utility functions that apply to all task classes, and protected virtual functions that must be overridden by each specific task class.
The public member functions of the abstract class Task apply to all task classes. Specific task classes must not hide them. A task object may call its own utility functions as well as those of other task objects.
Note that calling the member functions has_activated(), has_terminated(), is_active(), and get_priority() of another task object is unsafe. By the time the function returns a result, the queried property of the other task object may already have changed.
Specific task classes are defined in an application program as follows:
class SpecificTask : public Task
{
...
};
A specific task class may have multiple base classes (i.e. base classes other than Task), but that is not recommended.
Each specific task class must override the protected virtual member functions body() and virtual_sizeof(), and it may override the protected virtual member functions stack_size() and priority().
In addition to the member functions it inherits, a specific task class may have its own public and private members. The public members of a task class serve task synchronization and communication; the private members of a task class implement its functionality. Below is a list of simple rules on what public and private members a specific task class must have, may have, and must not have.
1. The definition of a specific task class must have the following public members:
2. The definition of a specific task class may have (and usually has) the following public members:
3. The definition of a specific task class must not have any public member functions!
This is very important, it goes to the core of the Ada tasking implementation. So let me elaborate:
Every task object is associated with a thread. The task thread is created and activated upon construction of the task object and then executes the task's body() function. Now, the Ada tasking implementation is based on the assumption that a task's private members can only be accessed by that one, its own thread. That has far reaching implications, for example: Private member variables of a task don't need to be declared volatile, don't need to be protected with mutexes, can't be corrupted by errors in other tasks, etc.
Hence we need to make sure that the private member functions of a task are only called - directly or indirectly - by the body() function. Public member functions would obviously make it easy to violate that restriction. Note that the Ada programming language enforces the same rule syntactically.
4. The definition of a specific task class may define private member functions and private member variables.
Such private members are very common and help to implement the task's functionality. It is possible though, to implement the entire functionality of a task in its body() function.
Note that a task's private member functions can only be executed by the task's own thread - and not by the thread of another task. Therefore, within a private member function (and the function body()) the following equality always holds true: this == current_task()
5. Be careful when defining static member variables.
Static member variables only exist once and are shared among all objects of a specific task class. Static member variables should only be used to implement conceptual constants (objects that are written exactly once) and not for the purpose of synchronization or communication between task objects of that class. The same applies to local static variables of task member functions. Such static variables should always be declared as volatile.
Initializing static variables within a task class is often subject to race conditions. If the static variable is initialized by a static constructor, there is no way to guarantee that initialization occurs before the first task tries to use it. If initialization is performed by the first task using the static variable, several task may try to initialize it at the same time.
The header file "Task.h" defines the global, task-related utility functions current_task(), delay(), and terminate_all().
typedef DWORD Milliseconds;
typedef Task** TerminationGroup; // NULL-terminated
Task* current_task();
void delay(Milliseconds time);
void terminate_all(TerminationGroup pt_table);
These functions implicitly refer to the currently executing task, i.e. the task that - directly or indirectly - makes the function call.
The header file "Synchronization.h" defines entry classes, entry class templates, and the class Rendezvous. An application program relies on the declarations in "Synchronization.h" to implement task synchronization and communication.
The abstract class Any_Entry is the base class of all entry classes defined in an application program.
class Any_Entry
{
public:
int count();
};
typedef Any_Entry** EntryPtrTable;
Task objects synchronize and communicate with each other through entries. Each entry is an object of an entry class. The header file "Synchronization.h" provides a set of entry class templates that may be instantiated in an application program. Many different entry classes can thus be defined in an application program. All entry classes have a common base class, the class Any_Entry.
The public member function count() returns the
number of pending callers of the entry for which count()
is called.
Note that count() is unsafe. By the time the
function returns a value, the number of pending callers of that entry may already have
changed (due to new incoming calls and callers that have just timed out).
The type EntryPtrTable represents a NULL terminated array of entries of any kind. It is up to the application code to ensure that upon constructing an EntryPtrTable the last entry is in fact set to NULL.
Task objects interact by means of entries (really entry objects). A task may have any number of entries. All entries of a task class are declared as public member variables. For example
struct DataRecord { ... };
class SpecificTask : public Task
{
public:
Entry1<int> send_event;
Entry0 start;
Entry<DataRecord> request_data;
...
};
Each entry is an object of an entry class - one of the classes described below. All entry classes are derived from the class Any_Entry. The "Ada Tasking in C++" library supports three kinds of entries:
All entries may have parameters. Parameter types of normal entries are explicitly defined in the application code and associated with an entry class when it is instantiated. The parameters of message entries are of type MSG. Signal entries do not have parameters.
A task calls a normal entry of another task like a function, passing actual parameters and an optional timeout value. The actual parameters are passed by reference. The timeout is measured in milliseconds. Each entry call returns a boolean value indicating whether the call was successful. For example:
SpecificTask s;
int event = 55;
DataRecord input_data;
if (s.send_event(event, 500))
printf("Entry call successful");
...
s.request_data(input_data); // passed by reference
...
The calling task is suspended until the task to which the entry belongs can accept the call in a rendezvous. If the value of the timeout parameter is not INFINITE, the calling task only waits until the specified timeout elapses. An entry call without a timeout is always successful.
Entry is the entry class template for a normal entry class with arbitrary parameters. Parameters are passed to such entries in the form of a structure (formally named Parameters).
template <class Parameters>
class Entry : public Any_Entry
{
public:
bool operator()(Parameters ¶ms,
Milliseconds timeout = INFINITE);
Parameters &actual_parameters(Rendezvous &rv);
Parameters* actual_parameters_pointer(Rendezvous &rv);
};
The member functions actual_parameters() and actual_parameters_pointer() are only accessible from within the task to which the entry belongs, while it is performing a rendezvous with the caller of the entry. The functions return a reference to the actual parameter structure passed during the entry call.
Entry0 is the entry class of a normal entry with no parameters.
class Entry0 : public Any_Entry
{
public:
bool operator() (Milliseconds timeout = INFINITE);
};
Entry1 is the entry class template for a normal entry class with one parameter (formally named Parameter1).
template <class Parameter1>
class Entry1 : public Any_Entry
{
public:
bool operator()(Parameter1 &p1,
Milliseconds timeout = INFINITE);
Parameter1 &actual_parameter1(Rendezvous &rv);
};
The member function actual_parameter1() is only accessible from within the task to which the entry belongs, while it is performing a rendezvous with the caller of the entry. The function returns a reference to the actual parameter passed during the entry call.
Entry2 is the entry class template for a normal entry class with two parameters (formally named Parameter1 and Parameter2).
template <class Parameter1, class Parameter2>
class Entry2 : public Any_Entry
{
public:
bool operator()(Parameter1 &p1, Parameter2 &p2,
Milliseconds timeout = INFINITE);
Parameter1 &actual_parameter1(Rendezvous &rv);
Parameter2 &actual_parameter2(Rendezvous &rv);
};
The member functions actual_parameter1() and actual_parameter2() are only accessible from within the task to which the entry belongs, while it is performing a rendezvous with the caller of the entry. The functions return references to the actual parameters passed during the entry call.
Entry3 is the entry class template for a normal entry class with three parameters (formally named Parameter1, Parameter2, and Parameter3).
template <class Parameter1, class Parameter2,
class Parameter3>
class Entry3 : public Any_Entry
{
public:
bool operator()(Parameter1 &p1, Parameter2 &p2,
Parameter3 &p3,
Milliseconds timeout = INFINITE);
Parameter1 &actual_parameter1(Rendezvous &rv);
Parameter2 &actual_parameter2(Rendezvous &rv);
Parameter3 &actual_parameter3(Rendezvous &rv);
};
The member functions actual_parameter1(), actual_parameter2(), and actual_parameter3() are only accessible from within the task to which the entry belongs, while it is performing a rendezvous with the caller of the entry. The functions return references to the actual parameters passed during the entry call.
Entry4 is the entry class template for a normal entry class with four parameters (formally named Parameter1, Parameter2, Parameter3, and Parameter4).
template <class Parameter1, class Parameter2,
class Parameter3, class
Parameter4>
class Entry4 : public Any_Entry
{
public:
bool operator()(Parameter1 &p1, Parameter2 &p2,
Parameter3 &p3, Parameter4 &p4,
Milliseconds timeout = INFINITE);
Parameter1 &actual_parameter1(Rendezvous &rv);
Parameter2 &actual_parameter2(Rendezvous &rv);
Parameter3 &actual_parameter3(Rendezvous &rv);
Parameter4 &actual_parameter4(Rendezvous &rv);
};
The member functions actual_parameter1(), actual_parameter2(), actual_parameter3(), and actual_parameter4() are only accessible from within the task to which the entry belongs, while it is performing a rendezvous with the caller of the entry. The functions return references to the actual parameters passed during the entry call.
MsgEntry is the entry class for a message entry. Message entries have a message descriptor (of type MSG) as an implicit parameter. When a new message is posted to the task thread's message queue, Windows implicitly calls the task's message entry. Message entries cannot be called explicitly.
class MsgEntry : public Any_Entry
{
public:
MSG &actual_msg(Rendezvous &rv);
MSG* actual_msg_pointer(Rendezvous &rv);
MsgEntry();
};
The member functions actual_msg() and actual_msg_pointer() are only accessible from within the task to which the entry belongs, while it is accepting a message in a rendezvous. The functions actual_msg() and actual_msg_pointer() return a reference to the message posted in the message queue.
Note that accepting a message entry in a rendezvous implicitly performs a PeekMessage() or GetMessage(). Therefore, when a task gets access to the message, it has already been removed from the message queue. A task should not call PeekMessage() or GetMessage() again.
SignalEntry is the entry class template for a signal entry class. Each signal entry class is associated with a specific signal (formally named sig). When the application process receives a signal it implicitly calls the signal entry (if any) of the signal entry class the signal is associated with. Signal entries cannot be called explicitly.
template <int sig>
class SignalEntry : public Any_Entry
{
public:
SignalEntry();
};
A task may have many signal entries, but within an application program there should be only one signal entry for each signal.
A task performs a rendezvous to accept calls from its entries. Each rendezvous is implemented by an object of the class Rendezvous. While a task object (called the owner task) performs a rendezvous, it synchronizes and communicates with the task (called the caller task) that called one of its entries. Entry calls to each entry are accepted in the order in which they were received.
class Rendezvous
{
public:
Rendezvous(Any_Entry *pe0,
DWORD
alternative = UNCONDITIONALLY);
Rendezvous(Any_Entry *pe0, Any_Entry *pe1,
DWORD
alternative = UNCONDITIONALLY);
Rendezvous(Any_Entry *pe0, Any_Entry *pe1,
Any_Entry *pe2,
DWORD
alternative = UNCONDITIONALLY);
Rendezvous(Any_Entry *pe0, Any_Entry *pe1,
Any_Entry *pe2, Any_Entry *pe3,
DWORD
alternative = UNCONDITIONALLY);
Rendezvous(EntryPtrTable pe_table,
DWORD
alternative = UNCONDITIONALLY);
bool successful();
int entry_selector();
};
To accept calls from its entries, a task declares an object of class Rendezvous in a local block. Such an object is called rendezvous object and the local block is called rendezvous block. The Rendezvous object is initialized with the set of entries (the rendezvous entries) from which the owner task wants to accept calls. By convention, the name of a rendezvous object is always accept. For example:
{
Rendezvous accept(&start, &send_event, &request_data);
...
}
Note that a task can only accept calls from its own entries - a restriction that is enforced at runtime. If a task attempts to perform a rendezvous using entries of another task, an exception is thrown.
The rendezvous with a caller task starts at the declaration of the rendezvous object and ends when the rendezvous object is destroyed, usually at the end of the rendezvous block. If the owner task leaves the rendezvous block via a goto, return, break statement or exception, the rendezvous ends at that point. Inside the rendezvous block the owner task may access the actual parameters passed by the caller and read or modify their values. The caller task remains suspended until the rendezvous ends. After the rendezvous both the owner task and the caller task are again executed in parallel.
Every time the owner task executes a rendezvous block it accepts one entry call from one of the rendezvous entries (or none if the rendezvous fails). If no call is pending, the owner task may wait until one of the rendezvous entries is called. If there are calls pending on several of the rendezvous entries, the owner task accepts one of those entry calls arbitrarily.
The class Rendezvous provides several constructors to create rendezvous objects. Each constructor takes as parameters:
By default the rendezvous alternative is UNCONDITIONALLY resulting in an unconditional rendezvous. The entry pointers specified in a rendezvous constructor designate the rendezvous entries. The owner task will only accept an entry call from one of the rendezvous entries. Other entries of the owner task are not considered in that rendezvous, even if they have pending calls.
The rendezvous constructors that take one, two, three, and four individual entry pointers are convenient shorthands for the general rendezvous constructor that takes a table of entry pointers (of type EntryPtrTable). An entry pointer table must always be NULL terminated. The general rendezvous constructor can be used in an application program to create rendezvous objects with an arbitrary - or a non-static - numbers of rendezvous entries. For example:
{
Any_Entry* const pe_table[4]
= {&start, &send_event,
&request_data, NULL};
Rendezvous accept(pe_table);
...
}
An entry specified in a rendezvous constructor can be guarded with the guarded() function:
Any_Entry* guarded(Any_Entry* pe, bool guard);
If the guard expression evaluates to false, the task will not consider the guarded entry for accepting calls. Such a rendezvous entry is said to be closed. For example:
{
Rendezvous accept(guarded(&start, !has_started), OR_FAIL);
...
}
A rendezvous must have at least one open entry, unless the rendezvous is conditional. If all entries of a rendezvous are closed and the rendezvous is not conditional, an exception is thrown.
A task can perform four different kinds of rendezvous. The kind of rendezvous is selected by the final, optional alternative parameter (the rendezvous alternative) of each rendezvous constructor. A rendezvous can either be unconditional, conditional, timed, or terminable:
The different rendezvous alternatives are defined as macros:
#define UNCONDITIONALLY
INFINITE
#define OR_FAIL
0
#define OR_DELAY(timeout) timeout
#define OR_TERMINATE
INFINITE-1
The different rendezvous alternatives are mutually exclusive. They are not bit-masks that can be combined with the | operator. Note that a timed rendezvous with a timeout of 0 is in fact a conditional rendezvous.
The class Rendezvous defines two member functions: successful()and entry_selector(). The member functions let the owner task retrieve information about the rendezvous it is currently performing. The member functions are only accessible by the owner task and only from within the rendezvous block.
The header file "Exception.h" defines the class Exception and other utilities related to exceptions. An application program relies on the definitions in "Exception.h" to catch runtime error exceptions and possibly recover from them.
The class Exception is a collection of static definitions regarding exceptions. Among other things the class Exception defines:
class Exception
{
public:
// Any_Error:
typedef Any_ErrorDescr *Any_Error;
static const char* name(Any_Error e);
static char* message(Any_Error e);
// RendezvousError:
typedef RendezvousErrorDescr *RendezvousError;
typedef AcceptErrorDescr
*AcceptError;
typedef NotATaskErrorDescr *NotATaskError;
typedef ForeignEntryErrorDescr *ForeignEntryError;
typedef ParameterErrorDescr *ParameterError;
static void throw_AcceptError(char* message);
static void throw_NotATaskError(char* message);
static void throw_ForeignEntryError(char* message);
static void throw_ParameterError(char* message);
// TaskingError:
typedef TaskingErrorDescr *TaskingError;
typedef EntryCallErrorDescr *EntryCallError;
typedef ActivationErrorDescr *ActivationError;
static void throw_EntryCallError(char* message);
static void throw_ActivationError(char* message);
// Any_Escape:
typedef Any_EscapeDescr *Any_Escape;
};
No objects of class Exception should be created in an application program. All members of the class Exception are static and must be referenced with qualified names.
Some Ada tasking functions throw an exception when they cannot complete normally and cannot return a reasonable result. Such exceptions are called runtime error exceptions. The runtime error exception types are nested types defined within the class Exception.
The exception type RendezvousError includes the following specific exception types:
Conceptually, these exception types are sub-types of RendezvousError, which is itself a sub-type of Any_Error.
The exception type TaskingError includes the following specific exception types:
Conceptually, these exception types are sub-types of TaskingError, which itself is a sub-type of Any_Error.
Note that any thread - whether it belongs to a task or not - can make an entry call. Such an entry calls are not considered to be errors.
Each runtime error exception has a name and a message explaining the reason why it was thrown. To retrieve these runtime error attributes, the static member functions name() and message() of the class Exception can be used. Of course, they may only be called from within an exception handler. For example:
catch (AcceptError e) {
log.record_runtime_error(Exception::message(e));
}
The member function name() is useful when an entire set of runtime error exceptions is caught with a single exception handler. For example:
catch (Any_Error e) {
if (strcmp(Exception::name(e), "EntryCallError") == 0) {
// Handle EntryCallError specially
}
}
For the sake of completeness, the class Exception provides public member functions to throw runtime error exceptions. Each function takes a parameter of type char* explaining the reason for the exception.
These functions should be used in place of the throw statement. It is unlikely though, that an application program will ever need them. The ability to explicitly throw runtime error exceptions might perhaps be useful during unit testing in a test stub.
Some Ada tasking functionality (specifically task termination) is currently implemented with exceptions. These exceptions are called escape exceptions and they are normally invisible to the application program; i.e. they are thrown and handled internally, within code that belongs to the "Ada Tasking in C++" library. The escape exception types are defined as nested types within the class Exception. The exception type Any_Escape covers (includes as sub-types) all escape exception types of the Ada tasking implementation.
Application code should never catch escape exceptions - either explicitly or implicitly. Escape exceptions that are caught by application code must be re-thrown! Note that application code can implicitly catch escape exceptions using the "..." syntax in a catch clause. For example:
catch (...) {
// Do some cleanup
throw;
}
It is important that all paths through an exception handler that catches ALL exceptions end with a throw statement. If an application program needs to catch ALL exceptions without re-throwing them, it must use the CATCH_ALL macro (see Catching ALL Exceptions) and not the catch (...) clause.
It is possible in C++ to write exception handlers that catch ALL exceptions. This is done with the ellipsis notation catch (...) in a catch clause. Such exception handlers are important for code that must never fail and recover from all exceptional situations. However, in an application program that uses the "Ada Tasking in C++" library, the ellipsis notation must not be used in an exception handler.
Instead, the macro CATCH_ALL must be used to catch ALL exceptions. The macro is defined as:
#define CATCH_ALL catch (Exception::Any_Escape) {throw;}
catch (...)
#define CATCH_ANY CATCH_ALL
#define CATCH_OTHER CATCH_ALL
The macros CATCH_ANY and CATCH_OTHER are synonyms of CATCH_ALL and may be used interchangeably. For example:
try {
// Some code that inconveniently throws exceptions
}
catch (char* s) {
// Catch specific types of exceptions
}
CATCH_ALL {
// Handle all other exceptions
}
Note that the CATCH_ALL handler must be the last handler of the handler list following a try statement.