C++言語解説:2-6.例外処理
2002-08-01

[概要] エラー処理の基本となる例外処理について学習します。C++の例外処理は多種有り
    ますが、ここでは標準的処理を対象とします。
    
[構成]・例外処理の基礎
     * try, throw, catch
     * 派生クラスのcatch
     * 全てをcatch
     * 投入型制限
     * 再投入
   ・newとdeleteの例外処理
     * newの例外処理
     * newとdeleteのオーバーロード
     * 脱線:auto_ptr


例外処理の基礎

 try, throw ,catch
 
  ・C++は実行時のエラーを操作するために、例外処理(exception handling)の機能を持
   っています。これにより例外エラー発生時に処理ルーチンを自動的に呼び出すこと
   ができるようになります。
  
  ・C++の例外処理は、try, throw, catchのキーワードで構成されます。一般的な構成
   は以下のようになります。
  
   [List1.tryとcatchの構成]
   ┌───────────────────────────────────┐
    try {
     //実行コードブロック
     //型1〜型Nの例外をthrowする可能性がある
    }
    catch(型1 仮引数){
     //例外処理ブロック1
    }
    catch(型2 仮引数){
     //例外処理ブロック2
    }
      :
      :
    catch(型N 仮引数){
     //例外処理ブロックN
    }     
   └───────────────────────────────────┘
   
  ・tryブロックにはエラーが監視されるべき実行コードを記述します。監視する部分は
   単一ステートメントでも、main()関数を含むプログラム全体でも構いませんが、実
   行時エラー処理を行う目的において、後者のみのtry〜catchで済むことは無いでし
   ょう。
  
  ・そして、エラー(例外)はtryブロックからthrowされます。言い換えると、throw
   定型の例外を生成
します。生成された例外型を処理するcatchブロックが存在する場
   合
、該当ルーチンで例外処理が実行されます。
  
  ・throwされた例外型を処理するcatch文が存在しない場合「プログラム異常終了」処
   理ハンドラ
terminate()関数が呼び出されます。terminate()関数が扱うエラー処
   理関数はコンパイラによって独自定義できますが、デフォルトはabort()関数を呼び
   出します。
  
  ・それではList2に例外処理の例を示します。

   [List2.例外処理の例]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    int main()
    {
      int i_a;
      cout << "int例外(0), char例外(1) : ";
      cin >> i_a;
      
      try {
        if (i_a==0) throw 100;      //int例外throw
        else if (i_a==1) throw 'A';   //char例外throw
        else if (i_a==2) throw "String"; //char*例外throw
        
        cout << "例外が起きなかった" << endl;
      }
      catch(int i_except){
        cout << "Catch int例外" << endl;
      }
      catch(char c_except){
        cout << "Catch char例外" << endl;
      }
      
      cout << "catchブロック終了" << endl;
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     C:>prog
     int例外(0), char例外(1) : 0
     Catch int例外
     catchブロック終了

     C:>prog
     int例外(0), char例外(1) : 1
     Catch char例外
     catchブロック終了
    
     C:>prog
     int例外(0), char例外(1) : 2
     Abnormal program termination ← abort()関数が実行された
    
     C:>prog
     int例外(0), char例外(1) : 3
     例外が起きなかった
     catchブロック終了
   └───────────────────────────────────┘
  
  ・List2では、int型とchar型のエラーハンドラを用意しています。0/1の入力により、
   int/charの例外がthrowされた段階でtryブロックの実行が終了し、「例外が起きな
   かった」は表示されていません。
  
  ・2を入力すると文字列(char*)例外をthrowしますが、char*に対するエラーハンドラ
   は用意していないので、terminate()→abort()関数が呼び出されています。



 派生クラスのcatch
 
  ・派生クラスをcatchする場合には注意が必要です。ポリモーフィズムの観点から見れ
   ばわかるのですが、基本クラスの例外は派生クラスの例外を含みます。
  
  ・従って、派生クラスの例外をcatchしたい場合、派生クラスのエラーハンドラを、基
   本クラスのエラーハンドラよりも先に記述
する必要があります。List3に失敗例を示
   します。

   [List3.派生クラスの例外処理(失敗例)]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Base {
      public:
        virtual void Exe() { cout << "Base\n"; return; }
    };
    
    class Derived : public Base {
      public:
        void Exe() { cout << "Derived\n"; return; }
    };
    
    int main()
    {
      Base base;
      Derived derived;
      
      int i_a;
      cout << "Base例外(0), Derived例外(1) : ";
      cin >> i_a;
      
      try {
        if (i_a) throw derived; //Derived例外
        else throw base;    //Base例外
      }
      catch(Base base){
        cout << "Catch Base例外" << endl;
      }
      catch(Derived derived){
        cout << "Catch Derived例外" << endl;
      }
      
      cout << "catchブロック終了" << endl;
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     C:\Tmp>prog
     Base例外(0), Derived例外(1) : 0
     Catch Base例外
     catchブロック終了

     C:\Tmp>prog
     Base例外(0), Derived例外(1) : 1
     Catch Base例外 <-------- Baseのハンドラが呼ばれている!!
     catchブロック終了
   └───────────────────────────────────┘
  
  ・ちなみにbcc32で、List3をコンパイルしたところ
     「'Derived' のハンドラが直前のハンドラ 'Base' で隠されている」
   と警告が出ました。List3の不具合を完全に言い当てています。
  
  ・ではList4に成功例を示します。

   [List4.派生クラスの例外処理(成功例)]
   ┌───────────────────────────────────┐
    //
    // BaseとDerivedについてはList3と同様
    //
    int main()
    {
      Base base;
      Derived derived;
      
      int i_a;
      cout << "Base例外(0), Derived例外(1) : ";
      cin >> i_a;
      
      try {
        if (i_a) throw derived; //Derived例外
        else throw base;    //Base例外
      }
      catch(Derived derived){
        cout << "Catch Derived例外" << endl;
      }
      catch(Base base){
        cout << "Catch Base例外" << endl;
      }
      
      cout << "catchブロック終了" << endl;
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     C:\Tmp>prog
     Base例外(0), Derived例外(1) : 0
     Catch Base例外
     catchブロック終了

     C:\Tmp>prog
     Base例外(0), Derived例外(1) : 1
     Catch Derived例外
     catchブロック終了
   └───────────────────────────────────┘


 全てをcatch
 
  ・全ての例外をcatchする以下の表現が存在します。
     catch(...){・・・}

  ・大抵は特定の型のエラーハンドラを記述後、取りこぼしたエラーの処理を行うため
   に使用します。List5に例を示します。

   [List5.catch(...)の例]
   ┌───────────────────────────────────┐
    //
    //List2にcatch(...)を追加
    //
    #include <iostream>
    using namespace std;
    
    int main()
    {
      int i_a;
      cout << "int例外(0), char例外(1) : ";
      cin >> i_a;
      
      try {
        if (i_a==0) throw 100;      //int例外throw
        else if (i_a==1) throw 'A';   //char例外throw
        else if (i_a==2) throw "String"; //char*例外throw
        
        cout << "例外が起きなかった" << endl;
      }
      catch(int i_except){
        cout << "Catch int例外" << endl;
      }
      catch(char c_except){
        cout << "Catch char例外" << endl;
      }
      catch(...){
        cout << "Catch 全ての例外" << endl;
      }
      
      cout << "catchブロック終了" << endl;
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     C:>prog
     int例外(0), char例外(1) : 0
     Catch int例外
     catchブロック終了
    
     C:>prog
     int例外(0), char例外(1) : 2
     Catch 全ての例外 <---------- char*の例外を捕捉している
     catchブロック終了
   └───────────────────────────────────┘
   

 投入型制限
 
  ・関数が外部へ投入する例外の型を制限することができます。例外型制限は以下のよ
   うに記述します。
     戻値型 関数名(引数リスト) throw(型リスト) {・・・}
  
  ・throw型リスト以外の例外を投入すると、unexpected()関数が呼ばれます。デフォル
   トでは、この関数もabort()を呼び出します。unexpected()が呼び出す関数もコンパ
   イラ側でユーザ定義することができます。

   [List6.投入型制限の例]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    void Throw_Except() throw(int, char);
    
    int main()
    {
      try {
        Throw_Except();
        cout << "例外が起きなかった" << endl;
      }
      catch(int i_except){
        cout << "Catch int例外" << endl;
      }
      catch(char c_except){
        cout << "Catch char例外" << endl;
      }
      catch(...){
        cout << "Catch 全ての例外" << endl;
      }
      
      cout << "catchブロック終了" << endl;
      
      return 0;
    }
    
    void Throw_Except() throw(int, char)
    {
      int i_a;
      cout << "int例外(0), char例外(1) : ";
      cin >> i_a;

      if (i_a==0) throw 100;      //int例外throw
      else if (i_a==1) throw 'A';   //char例外throw
      else if (i_a==2) throw "String"; //char*例外throw
      
      return;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     C:>prog
     int例外(0), char例外(1) : 0
     Catch int例外
     catchブロック終了
    
     C:>prog
     int例外(0), char例外(1) : 2
     Abnormal program termination <---- catch(...)も捕捉しない
   └───────────────────────────────────┘
  
  ・List6では、2が入力されると必ずunexpected()が実行されます。bcc32では
     throw 式が例外指定に違反している(関数 Throw_Except() throw(int,char) )
   の警告がコンパイル時に出ました。


 再投入
 
  ・catchブロック(エラーハンドラ)から例外を再投入(rethrow)することができます。
   このときは例外オブジェクトを指定せずに、そのままthrowと記述します。このとき
   throwするものは該当catchが捕捉した例外オブジェクトです。
  
  ・これは関数の中でローカルなtry〜catchブロックがあり、その中でcatchした例外の
   ローカル処理を施した後、再度関数の外へ再投入するようなケースで使用します。
  
  ・これにより、エラー処理を適当な分担を持たせながら、複数のハンドラへ分散させ
   ることができます。
  
  ・尚再投入時に投入すべきオブジェクトが存在しない場合、terminate()関数が呼び出
   されます。



newとdeleteの例外処理

 newの例外処理
 
  ・1-7章(他のデータ型と演算子)でnewとdelete演算子を学習しましたが、このときは
   「newによるメモリ確保は成功」という前提で説明を行いました。もちろん実際は失
   敗することも有り得ます。

  
  ・最新のC++では、new失敗時に例外が投入されます。これは「古いC++は違った」とい
   う意味です。これからC++を使用するのであれば、例外方式を理解すべきと考えます。
   
  ・newはメモリ確保に失敗するとbad_alloc例外を投入します。bad_alloc例外を使用す
   るには<new>ヘッダが必要です。ではList7に例を示します。
   
   [List7.newの例外処理]
   ┌───────────────────────────────────┐
    #include <iostream>
    #include <new>
    using namespace std;
    
    int main()
    {
      double* dp;
      int i_num;
      
      cout << "確保数 :";
      cin >> i_num;
      
      try {
        dp = new double[i_num];
        cout << "確保成功" << endl;
      }
      catch(bad_alloc ba) {
        cout << "確保失敗" << endl;
        exit(EXIT_FAILURE);
      }
      
      delete[] dp;
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     C:>prog
     確保数 :1000
     確保成功

     C:>prog
     確保数 :250000000
     確保失敗
   └───────────────────────────────────┘


 newとdeleteのオーバーロード
 
  ・newとdeleteは演算子ですから、当然オーバーロードすることができます。しかし
   2-2章(演算子オーバーロード)で説明しなかった理由ですが、newの標準的動作とし
   て確保失敗時にbad_allocを投げる必要
があったからです。
  
  ・オーバーロード版のnewは、意図的にオーバーロード関数内で定義をしない限り
   bad_alloc例外を投げません。
確保失敗時にはnullポインタが返ります。
  
  ・このままだと、malloc()やfree()(*1)と何も違わない印象を持ちますが、決定的に
   違う点があります。それは
     new / newのオーバーロード --------> コンストラクタを呼ぶ
     delete / deleteのオーバーロード --> デストラクタを呼ぶ
   ことです。
  
  ・もう1点ですが、配列が呼ばれると、つまりnew[] / delete[]が呼ばれると、各オブ
   ジェクトの確保数分、コンストラクタ又はデストラクタが自動的に呼ばれます。

   ログラマが特に意識する点はありません。
   
  ・それではList8にnewとdeleteのオーバーロード例を示します。ここでは、オーバー
   ロードのnewと通常のnewが混乱するのを避けるために、メモリ確保そのものは
   malloc()とfree()関数で行っています。
  
   [List8.newとdeleteのオーバーロード]
   ┌───────────────────────────────────┐
    #include <iostream>
    #include <new>
    using namespace std;
    
    class Sample {
      public:
        Sample() { cout << "コンストラクタ\n"; return; }
        ~Sample() { cout << "デストラクタ\n"; return; }
        void* operator new(size_t st_size);
        void* operator new[](size_t st_size);
        void operator delete(void* p_mem);
        void operator delete[](void* p_mem);
    };
    
    void* Sample::operator new(size_t st_size)
    {
      void* p_mem;
      
      cout << "--- new実行 ---\n";
      p_mem = malloc(st_size);
      if (!p_mem) {
        bad_alloc ba;
        throw ba; //bad_alloc例外の投入
      }
      return p_mem;
    }

    void* Sample::operator new[](size_t st_size)
    {
      void* p_mem;

      cout << "--- new[]実行 ---\n";
      p_mem = malloc(st_size);
      if (!p_mem) {
        bad_alloc ba;
        throw ba; //bad_alloc例外の投入
      }
      return p_mem;
    }
    
    void Sample::operator delete(void* p_mem)
    {
      cout << "--- delete実行 ---\n";
      free(p_mem);
      return;
    }

    void Sample::operator delete[](void* p_mem)
    {
      cout << "--- delete[]実行 ---\n";
      free(p_mem);
      return;
    }
    
    int main()
    {
      Sample* p_new1;
      Sample* p_new2;
      
      try {
        p_new1 = new Sample;
      }
      catch(bad_alloc ba){
        cout << "失敗1\n";
        exit(1);
      }
      delete p_new1;
      
      try {
        p_new2 = new Sample[2]; //ここを変えると失敗例が見える
      }
      catch(bad_alloc ba){
        cout << "失敗2\n";
        exit(1);
      }
      delete[] p_new2;
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     --- new実行 ---
     コンストラクタ
     デストラクタ
     --- delete実行 ---
     --- new[]実行 ---
     コンストラクタ
     コンストラクタ
     デストラクタ
     デストラクタ
     --- delete[]実行 ---
   └───────────────────────────────────┘
  
  ・この実行結果から、演算子関数とコンストラクタ/デストラクタが動作する順番を確
   認して下さい。
  

 脱線:auto_ptr
 
  ・newとdeleteが伴うオブジェクトを扱うために
     コピーコンストラクタ
     代入演算子オーバーロード
   等を2-1,2-2章で学習してきました。これらは暗黙のコピーや、暗黙の代入演算によ
   り解放すべきメモリ領域が2重指定されることが要因
でした。
  
  ・しかしながら、メモリ容量的に厳しいオブジェクトを使用する場合、従来のように
   コピーコンストラクタや代入演算子関数で「別領域」を確保できるものでしょうか。
  
  ・この場合、C++ではテンプレートクラスのauto_ptrを使用します。auto_ptrの特徴は
     コピー時に所有権が移る
     スコープ終了時にdeleteする
   といった点でしょう。尚auto_ptrを使用するには<memory>ヘッダが必要です。
  
  ・auto_ptrは以下の書式で使用します。
   ┌───────────────────────────────────┐
     (1)newで確保したオブジェクトを指す場合
       auto_ptr<型名> ポインタ名(new 型名);
     (2)何も指さない場合
       auto_ptr<型名> ポインタ名;
   └───────────────────────────────────┘
   List9に所有権が移る例を示します。
  
   [List9.auto_ptr使用例(1):所有権が移る]
   ┌───────────────────────────────────┐
    #include <iostream>
    #include <memory>
    #include <new>
    using namespace std;
    
    class Sample {
       int mi;
      public:
       Sample(int i_num){
         mi = i_num;
         cout << mi;
         cout << " :コンストラクタ\n";
         return;
       }
       ~Sample(){
         cout << mi;
         cout << " :デストラクタ\n";
         return;
       }
    };
    
    int main()
    {
      auto_ptr<Sample> p_sample1(new Sample(1));
      auto_ptr<Sample> p_sample2;
      
      cout << "auto_ptrコピー前\n";
      
      p_sample2 = p_sample1; //auto_ptrの代入
      
      cout << "auto_ptrコピー後\n";
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     1 :コンストラクタ
     auto_ptrコピー前
     auto_ptrコピー後
     1 :デストラクタ
   └───────────────────────────────────┘
  
  ・この結果からわかるように、コンストラクタとデストラクタは1回ずつ実行されてい
   ます。つまりp_sample1が指していたオブジェクトが、p_sample2へ所有権委譲され
   ています。
そしてp_sample2のスコープ終了時にデストラクタが実行されています。
  
  ・これが通常のポインタであれば、p_sample1とp_sample2は同じオブジェクトを指し
   必ずdeleteを行う必要から、どちらのポインタをdeleteするべきか、注意を払わな
   ければなりません。
   
  ・auto_ptrの場合、deleteはオブジェクトのスコープが失われた時点で実行されます。
   List10に例を示します。
  
   [List10.auto_ptr使用例(2):スコープ終了時にdelete]
   ┌───────────────────────────────────┐
    //
    // List9と同様
    //
    int main()
    {
      auto_ptr<Sample> p_sample1(new Sample(1));
      auto_ptr<Sample> p_sample2(new Sample(2));
      
      cout << "auto_ptrコピー前\n";
      
      p_sample2 = p_sample1; //auto_ptrの代入
      
      cout << "auto_ptrコピー後\n";
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     1 :コンストラクタ
     2 :コンストラクタ
     auto_ptrコピー前
     2 :デストラクタ
     auto_ptrコピー後
     1 :デストラクタ
   └───────────────────────────────────┘

  ・p_sample2 = p_sample1が実行された段階で、p_sample2にSample(1)オブジェクトの
   所有権が移り、上書きされたSample(2)オブジェクトのスコープが終了するので
   Sample(2)のデストラクタが実行
されます。
  
  ・このように、auto_ptrを使用することで、newとdeleteに伴うコーディングミスを減
   らすことができるようになります。使いこなせれば非常に便利な機能でしょう。
  
  ・ただし注意点として、auto_ptrは自らがdeleteを行う機能(破壊的コピーセマンティ
   クス-destructive copy semantics)を持っているため、STLのコンテナ要素等には適
   用できません。

  
  ・STLについては、本テキストでは範囲外としていますが、今後違う場にて取り扱いた
   いと思っています。


(*1)malloc()はC言語で動的メモリを確保する関数です。戻り値はvoid*ポインタです。
  free()は引数のポインタが指すメモリを解放する関数です。

[Revision Table]
 |Revision |Date    |Comments
 |----------|-----------|-----------------------------------------------------
 |1.00   |2002-08-01 |初版
 |1.01   |2002-08-08 |リンク追加
[end]
Copyright(C) 2002 Altmo