Tools.h++ 7.0 ユーザーズガイド: 16 - エラー処理

Copyright 1999 Rogue Wave Software
Copyright 1999 Sun Microsystems, Inc.

# エラー処理 | # 16 |

エラー処理はやっかいで、あまり考えたくないテーマでしょう。しかし、しっかりしたコードを書こうとするなら、やはり考えなければなりません。

Rogue Wave クラスライブラリはすべて、同じ、広範囲におよぶ完成されたエラー処理機能を使用します。このモデルでは、エラーは内部エラーと外部エラーの 2 つのカテゴリに大きく分類されます。内部エラーは、さらに回復可能または回復不可能のいずれかに分類され、プログラムの内部論理のエラーによって起こります。この種のエラーは通常、回復するのが難しく、プログラムを異常終了するのがデフォルトの応答です。外部エラーは、そのプログラムが予想していなかった事によって起こります。重要なプログラムは、外部エラーからいつでも回復できるようにしておく必要があります。

次の節では、Rogue Wave のエラーモデルを要約する表を示します。表に続いて、各種のエラーについて詳しく説明します。

## Tools.h++ のエラーモデル

次の表は、Tools.h++ エラーモデルのエラーを分類して説明したものです。この表は、早見表として使用したり、参照として使用することができます。

#### 表 16-1 Tools.h++ のエラーの種類の比較

エラーの種類 | 内部エラー | 外部エラー
--- | --- | ---
| 回復不可能 | 回復可能 |
原因 | プログラムの論理またはコードの欠陥 | プログラムの論理またはコードの欠陥 | プログラムが予想していなかった事
例 | 境界エラー、コレクションに `NULL` ポインタを挿入した | リンクリストの境界エラー、無効な日付を使用しようとした | 間違った日付を書き込もうとした、正則でない行列の逆行列を求めようとする、ストリーム書き込みエラー、メモリー不足
予測可能かどうか | 可能 | 可能 | 不可能
検出に要するコスト | 高い | 低い | 低い
抽象レベル | 低い | 低い | 高い
検出場所 | ライブラリのデバッグバージョン | ライブラリのデバッグバージョンと本番バージョン | ライブラリのデバッグバージョンと本番バージョン
応答 | 回復機構なし | RWInternalErr から継承する例外を送出する | RWExternalErr から継承する例外を送出するか、オブジェクトの妥当性検査を行う

## 内部エラー

内部エラーは、プログラム内の論理やコーディングの欠陥によって起こります。一般的な内部エラーは次のとおりです。

- 境界エラー
- コレクションへの `NULL` ポインタの挿入
- 間違った日付の使用

これらのエラーはすべて防止することができます。たとえば、配列のインデックスの許容範囲は常にわかっているので、境界エラーを避けることができます。また、プログラムでの間違った日付の使用は、明らかな論理エラーとして訂正することができます。

内部エラーは、エラー検出のコストと、そのエラーが実行時に検出されるかどうかによって、さらに次の 2 つに分類することができます。

- 回復不可能な内部エラー
- 回復可能な内部エラー

### 回復不可能な内部エラー

回復不可能な内部エラーは、次のような共通の特性があります。

- 予測しやすい
- 比較的低いレベルで発生する
- 検出コストがかかる
- ライブラリのデバッグバージョンでのみ検出される

回復不可能な内部エラーには、定義上、回復機構がありません。これらのエラーの例としては、境界エラーと、コレクションへの `NULL` ポインタの挿入があります。

なぜ、ライブラリはいくつかのエラーを回復不能と定義するのでしょうか。それは、エラーの検出には時間がかかるからです。性能上の理由から、ライブラリは、プログラムの一部に対して最小限の正確さだけを要求し、そして、そのレベルに達しないものだけをエラーであると判断します。エラーが回復不可能であるというのは、ライブラリの本番バージョンがそのようなエラーを検出する機構を持たない (したがって、エラーを回復することもない) という意味からです。

境界エラーは回復不可能です。なぜなら、インデックスが範囲内にあることを確認するためのコストは、配列へのアクセスそのもののコストをはるかに超える可能性があるからです。配列アクセスの多いプログラムでは、すべてのアクセスを検査すると、プログラムの処理速度が低下する可能性があります。これを避けるために、ライブラリは、ユーザーが常に有効なインデックスを使用するよう要求します。要求されている正確さが最小レベルであるので、回復不可能なエラーは比較的避けやすく、単純な概念のものです。

ライブラリのデバッグバージョンを使用してアプリケーションのコンパイルとリンクを行うと、回復不可能なエラーを簡単に見つけて除去することができます。詳細については、408 ページの「Tools.h++ のデバッグバージョン」を参照してください。デバッグバージョンには、コーディングエラーを発見するように設計された追加検査が多数含まれています。これらの検査には、余分な時間を要するものや、デバッグメッセージを出力するものまであるので、効率の良い最終製品を作成するには、本番バージョンを使用してコンパイルとリンクを行うとよいでしょう。

デバッグバージョンのライブラリがエラーを発見すると、通常は、プログラムを異常終了させます。

### 回復可能な内部エラー

回復可能な内部エラーは、予測しやすく低いレベルで起こるという点で回復不可能なエラーに似ています。しかし、次の点が異なります。

- 検出コストが低い
- ライブラリのデバッグバージョンと本番バージョンの両方で検出される

リンクリストの境界エラーや無効な日付の使用は、いずれも回復可能な内部エラーの例としてあげられます。このようなエラーに対する応答として、ライブラリは、RWInternalErr から継承する例外を発行します。

本番バージョンのライブラリは、検査のコストが比較的低いので、回復可能な内部エラーを検査することができます。たとえば、リンクリストの境界エラーを見つける場合、リスト内を検索するコストは、インデックスが境界内にあるかどうかを検出するコストをはるかに上回ります。したがって、アクセスのたびに境界エラーを検査することができます。

エラーが検出された場合、すでに説明したように、ライブラリは、RWInternalErr から継承する例外を送出します。次に、Tools.h++ が例外を送出した場合を示します。

      // リンク "i" を探す。インデックスは範囲内になければならない
      RWIsvSlink* RWIsvSlist::at(size_t i) const
      {
         if (i >= entries()){
           if(RW_NPOS == i)
             RWTHROW( RWBoundsErr( RWMessage( RWTOOL_NPOSINDEX)));
           else
             RWTHROW( RWBoundsErr( RWMessage( RWTOOL_INDEXERR,
                     (unsigned)i,
                     (unsigned)entries()) ));
         }

         register RWIsvSlink* link = head_.next_;
         while (i--) link = link->next_;
         return link;
      }

このコードでは、関数が境界エラーをいつもどのように検出しようとするかに注意してください。関数は、エラーを検出すると、クラス RWInternalErr から継承する RWBoundsErr のインスタンスを送出します。このインスタンスには国際化されたメッセージが含まれています (370 ページの「メッセージの地域化」を参照)。`RWTHROW` マクロは、19.3.1 の節に説明してあります。

例外を送出することによって、例外を捕獲して、おそらくは回復する機会がユーザーに与えられます。しかし、プログラムの内部論理には疑いがあるので、作業中の文書を保存して、プログラムを中止するのがよいでしょう。

### 外部エラー

外部エラーは、プログラムが予想していなかった事が原因で起こります。概要で説明したように、重要なプログラムは外部エラーから回復できるようにしておく必要があります。外部エラーには次のものがあります。

- 簡単に予測できない
- より抽象的なレベルで起こる
- 検出コストがかからない
- ライブラリの本番とデバッグの両方のバージョンで検出される

外部エラーの例には、1992 年 6 月 31 日などの間違った日付を設定しようとした、正則でない行列の逆行列を求めようとした、ストリーム書き込みエラー、メモリー不足などがあります。Tools.h++ は、RWExternalErr から継承する例外を発行するか、オブジェクトの妥当性検査を行うことによって、これらのエラーに応答します。

外部エラーは実行時エラーの場合があります。オブジェクト指向環境では、実行時エラーは、無効なユーザー入力の結果、オブジェクトを無効な状態に設定しようという試みとして示される場合がよくあります。上記の例は、存在しない 1992 年 6 月 31 日という日付によって日付オブジェクトを初期設定する外部実行時エラーです。

外部エラーは、通常、オペレーティングシステムによって送出された例外という形式をとります。Tools.h++ は、これらの例外を検出して、獲得した資源を回復します。また、ファイルをクローズし、ヒープメモリーを復元します。しかし、ユーザーは、この種の例外中、Tools.h++ ライブラリの外部コードによって獲得された資源すべてに対して責任があります。一般に、Tools.h++ は、`memcpy` などのメモリ関連の C ライブラリ呼び出し中に、これらの例外が送出されないものと仮定します。Tools.h++ は、Tools.h++ オブジェクトまたはユーザー定義オブジェクトに対する操作中に発生する例外を検出しようとします。

理論上、Tools.h++ の外部エラーに対する応答は、例外の送出か、またはオブジェクト妥当性検査の実行です。プログラムを異常終了することはありません。しかし、実際には、コンパイラの中には例外を処理しないものがあるので、Tools.h++ は、エラーハンドラを回復する機会か、状態値をテストする機会を与えます。次に、`isValid` 関数を使用してユーザーの入力を検査する例を示します。

      RWDate date;

      while (1) {
         cout << "Give a date: ";
         cin >> date;
         if (date.isValid()) break;
         cout << "You entered a bad date; try again\n";
      }

## 例外アーキテクチャ

例外が発生すると、throw オペランドが渡されます。throw オペランドの型によって、それを捕捉できるハンドラが決まります。Tools.h++ は、throw オペランドに対して次の階層を使用しています。

    xmsg
        RWxmsg
            RWInternalErr
                RWBoundsErr
            RWExternalErr
                RWFileErr
                RWStreamErr
        xalloc
            RWxalloc

このように、この階層は 395 ページの「Tools.h++ のエラーモデル」で概要を述べたエラーモデルに相当します。この階層は、クラス `xmsg` の存在を前提にしています (通常はコンパイラベンダーによって供給されます)。これは、C++ 標準化委員会 X3J16 (92-0116 文書) のライブラリ作業グループによって標準化が検討されているクラスです。ご使用のコンパイラに `xmsg` と `xalloc` が付属していない場合は、Rogue Wave のクラス `RWxmsg` および `RWxalloc` がこれらをエミュレートします。

クラス `xmsg` は、何が間違っていたのかユーザーが見当をつけられるよう、捕獲側で出力できる文字列を渡します。この文字列は `xmsg` の特殊化バージョンによってフォーマットおよび国際化されます (370 ページの「メッセージの地域化」で説明しています)。

### エラーハンドラ

Tools.h++ は、マクロ `RWTHROW` を使用して例外を発行します。コンパイラが例外をサポートしている場合、このマクロは、例外を発行する関数を呼び出すことによって解釈処理されます。コンパイラが例外をサポートしていない場合、マクロはエラーハンドラを次のプロトタイプによって呼び出すよう解釈処理されます。

      void errHandler(const RWxmsg&);

デフォルトのエラーハンドラは、プログラムを異常終了させます。デフォルトハンドラは、次の関数によって変更することができます。

      typedef void (*rwErrHandler)(const RWxmsg&);
      rwErrHandler rwSetErrHandler(rwErrHandler);

次の例は、ユーザー定義のエラーハンドラが、例外をサポートしないコンパイラでどのように働くかを示しています。

    #include <rw/rwerr.h>
    #include <rw/coreerr.h>
    #include <iostream.h>

    #ifdef RW_NO_EXCEPTIONS

    void myOwnErrorHandler(const RWxmsg& error){
      cout << "myOwnErrorHandler(" << error.why() << ")" << endl;
    }

    int main(){
      rwSetErrHandler(myOwnErrorHandler);  // デフォルトエラーハンドラを
                                 // 獲得するためには、この行をコメントにする
      RWTHROW( RWExternalErr(RWMessage( RWCORE_GENERIC, 12345, "Howdy!") ));
      cout << "Done." << endl;
      return 0;
    }

    #else  //RW_NO_EXCEPTIONS

    #error This example only for compilers without exception handling

    #endif

## Tools.h++ のデバッグバージョン

Tools.h++ ライブラリは、デバッグモードで作成することにより、各自のコード内の内部エラーを回復、訂正するための非常に強力なツールになります。

このライブラリのデバッグバージョンを作成するには、プリプロセッサフラグ "`RWDEBUG`" を定義してライブラリ全体をコンパイルしなければなりません。ライブラリとアプリケーション全体のコンパイルには、このフラグを定義するか定義しないかというどちらかひとつの設定を使用しなければなりません。結果として生成されるライブラリとプログラムは、やや大きくて遅いものになります。その他の指示については、該当する `makefile` を参照してください。

フラグ `RWDEBUG` は、重大な関数と最初と最後にある `PRECONDITION` 句と `POSTCONDITION` 句の組を有効にします。これらの前条件と終了条件は、`assert` で実装されます。障害があると、障害となる条件と、その障害が発生したソースファイルの名前と行番号を出力した後でプログラムは停止します。

#### RWPRECONDITION と RWPOSTCONDITION

Bertrand Meyer 氏は、その著書『Object-oriented Software Construction』で、関数を呼び出し元と呼び出し先の間の契約と見なすよう示唆しています。呼び出し元が 1 組の前条件に従うことに同意した場合、呼び出し先は、1 組の終了条件を満たす結果を返すように保証します。次の比較は、Tools.h++ において Meyer 氏の命題が有効であることを示しています。まず、C における境界エラーについて考えてみます。

      char buff[20];
      char j = buff[20];                               // 境界エラー

このような境界エラーの検出は C では非常に困難ですが、C++ では次に示すように簡単です。

      RWCString buff(20);
      char j = buff[20];                     // 検出可能な境界エラー

C++ では `operator[]` を多重定義して明示的な境界検査を実行できるので、境界エラーの検出が容易です。つまり、フラグ `RWDEBUG` が設定されている場合、`operator[]` は次のように `PRECONDITION` 句も実行します。

      char& RWCString::operator[](size_t i){
         RWPRECONDITION(i < length() );
         return rep[i];
      }

ここで説明した状況では、`operator[]` が `PRECONDITION` との不適合を検出するので、障害が生じます。

次に、さらに複雑な例を示します。

      template <class T> void List::insert(T* obj){
         RWPRECONDITION( obj!= 0 );
         head = new Link(head, obj);
         RWPOSTCONDITION( this->contains(obj) );
      }

この例では、関数 `insert()` は、引数が指すオブジェクトを、型 `T` のオブジェクトへのポインタのリンクリストに挿入します。この関数が動作するために必要な唯一の前条件は、ポインタ `obj` が `NULL` ではないということです。この条件が満たされればは、関数は必ずオブジェクトを正常に挿入します。条件は終了条件句によって検査されます。

マクロ `RWPRECONDITION` と `RWPOSTCONDITION` は `<rw/defs.h>` に定義されていて、前処理マクロ `RWDEBUG` が定義されていなければ、非実行文としてコンパイルされます。次に、`defs.h` の内容を示します。

      #ifdef RWDEBUG
      #  define RWPRECONDITION(a)      assert(a)
      #  define RWPOSTCONDITION(a)     assert(a)
      #else
      #  define RWPRECONDITION(a)      ((void*)0)
      #  define RWPOSTCONDITION(a)     ((void*)0)
      #endif