はじめに
Effective C++ 第3版の8項 39ページから勉強していきます。
今回は、「デストラクタから例外を投げないようにしよう」についです。
Effective C++ 第3版 - 8項 デストラクタから例外を投げないようにしよう -
前置き
今回は、デストラクタで例外が起こる可能性がある場合の対策処理について勉強していきます。
今回の勉強内容
デストラクタでの例外
以下に示すように、「Connection」というデータベースへ接続をするクラスと「Manage」という接続を管理(接続・切断)するクラスを扱います。
class Connection {
 public:
  Connection() {}
  ~Connection() {}
  void open(){/* 接続を開始する処理 */};
  void close(){/* 接続を終了する処理 */};
};
class Manage {
 public:
  Manage() { conn_.open(); }
  ~Manage() {
    std::cout << "Manage : デストラクタ" << std::endl;
    conn_.close();
  }
 private:
  Connection conn_;
};
このコードは、データベースへの接続が正しく切断される限り、問題はありません。
しかし、もしデータベースへの切断に失敗し、例外が投げられると、Manageのデストラクタは例外を投げ、そこで、処理が中断されます。
そのため、トラブルになります。
この問題を避けるために、2つの対処方法があります。
1つめは、「デストラクタ中でプログラムを中止してしまう」です。
例外が投げられたら、abortなどを使い、強制的にプログラムを中止させます。
~Manage() {
     try {
       conn_.close();
     } catch (...) {
       /*      closeの失敗を記録する      */
       std::cout << "ERROR : " << __FILE__ << " : " << __LINE__ << std::endl;
       std::abort();  // 強制終了
     }
}
上のようにすることで、未定義の動作にはなりませんが、それ以降プログラムが続きません。
2つめは、「デストラクタが飲み込んでしまう」です。
例外が投げられたら、それをデストラクタが飲み込みます。
~Manage() {
     try {
       conn_.close();
     } catch (...) {
       /*      closeの失敗を記録する      */
       std::cout << "ERROR : " << __FILE__ << " : " << __LINE__ << std::endl;
     }
}
上のようにすることで、エラーが起こっても、それを無視してプログラムを正常に実行し続けます。
これらは、どちらも、closeが例外を投げた時に、その具体的な状況に対処するようになっていません。
そのため、Manageのクライアント(利用者)に「問題に対処する機会」を与えるようなデザインにしたほうが良いそうです。
class Manage {
 public:
  Manage() { 
    conn_.open();
    closed = false;
  }
  
  ~Manage() {
    if (!closed) {  // Connectionの接続状況を調べる
      try {
        conn_.close();
      } catch (...) {
        std::cout << "ERROR : " << __FILE__ << " : " << __LINE__ << std::endl;
      }
    }
  }
  void close() {  // データベースとの接続を切る関数を提供
    conn_.close();
    closed = true;
  }
 private:
  Connection conn_;
  bool       closed;
};
上のように、Manage自身が、データベースとの接続を切る関数を提供することによって、クライアントが自分で問題に対処する方法を決めることができます。
また、Manageオブジェクトが破棄されるときに、デストラクタでConnectionの接続を調べ、まだ切れていなければ、そこで切れるようにもできます。
これにより、「切り忘れ」を防ぐことができます。
サンプルコード
以下に、勉強で使用したコードを示します。
# include <iostream>
class Connection {
 public:
  Connection() {}
  ~Connection() {}
  void open(){/* 接続を開始する処理 */};
  void close(){/* 接続を終了する処理 */};
};
class Manage {
 public:
  Manage() {
    conn_.open();
    closed = false;
  }
  /*
  ~Manage() {
    std::cout << "Manage : デストラクタ" << std::endl;
    conn_.close();
  }
  */
  /*
   ~Manage() {
     try {
       conn_.close();
     } catch (...) {
       std::cout << "ERROR : " << __FILE__ << " : " << __LINE__ << std::endl;
       std::abort();
     }
   }
   */
  ~Manage() {
    if (!closed) {
      try {
        conn_.close();
      } catch (...) {
        std::cout << "ERROR : " << __FILE__ << " : " << __LINE__ << std::endl;
        std::abort();
      }
    }
  }
  void close() {
    conn_.close();
    closed = true;
  }
 private:
  Connection conn_;
  bool       closed;
};
int main() {
  std::cout << "8_qiita.cpp" << std::endl;
  Manage mg_;
}
実行結果
まとめ
今回は、デストラクタで例外が起こるような問題への対処について学びました。
例外が投げられ場合、「デストラクタ中でプログラムを中止してしまう」・「デストラクタが例外を飲み込む」ことによって未定義な動作を避けることができることを学びました。
また、Manageのクライアントに「問題に対処する機会」を与えることで、例外を投げた場合の処理をクライアントが選べれるというクラスのデザインにした方が良いことを学びました。
- デストラクタは例外を投げるのは良くない。
- もしデストラクタ内で例外を投げる関数を呼び出す場合、デストラクタがプログラムを中止させるか、その例外を捕捉し、処理するようにする。
- クラスのクライアントが例外に対処する必要があるなら、クラスに「例外を投げるかもしれない処理」をする通常の関数(デストラクタでない関数)を付ける。
メモ
例外とは
例外とは、異常事態の発生を通知するための仕組みです。
ここでいう異常状態とは、プログラムが予想していない事態(動作)を指します。
例としては、キーボードからの入力で数値を受け取るはずが、文字列が送られてしまい、int型に変換できず処理が止まる。
例外を投げるとは
異常状態が発生したことを検知したメゾットは例外を「投げ」ます。
これによって、異常事態が発生したことを知らせます。
try & catchの「try」の部分
例外を捕まえるとは
異常状態が発生したメゾットを呼び出した側のメゾッドでは、投げられた例外を「捕まえる」ことができます。
捕まえると、適切な処理を行うことができます。
try & catchの「catch」の部分
参考文献
[1] https://www.amazon.co.jp/gp/product/4621066099/ref=dbs_a_def_rwt_hsch_vapi_taft_p1_i0
[2] http://www.media.osaka-cu.ac.jp/~matsuura/2008-PP/Exception.html