C++ まとめ -言語編-

例外

例外

例外

例外とは,エラー処理からエラー報告を分離するための仕組み.このような仕組みがない場合,ライブラリの開発者は実行時にエラーを検出できるが,それをどのように処理したら良いか判断できない.逆にライブラリのユーザはそのようなエラーをどのように対処すれば良いか判断できるが,エラーを検出できない.

例えばある整数を別の整数で割って結果の整数を返す関数を定義しようとする.この時,数学的にゼロで除算することはできない(C++では,ゼロ除算を行った場合の振る舞いは,マシンに依存する).


int div(int x, int y) // 関数の製作者
{
  // if (y==0)でエラーを検知しても報告する手段がない
  return x / y; 
}

int main() // 関数のユーザ
{
  div(1, 0); // NG:ユーザは第2引数に0を与えないよう注意
  div(5, 2); // OK:
  return 0;
}
この時,関数内部では,関数に与えられた除数が0か否かを確認することで,エラーを検出できる.しかしいくつかのプログラミング言語では,これをユーザに通知する手段がない.そのため関数の作成者は,悪意ある利用やユーザのミスに対する警告を行うことができない.

詳細は後述するが,前述のコードに例外処理機構を追加すると次のようになる.なお,例外の種類と関数を関連付けるために名前空間を使用している.


namespace Div
{
  class zero_div{};
  int exe(int x, int y) throw (zero_div) // zero_div例外を発生する関数であることを宣言
  {
    if (y==0)
      throw zero_div(); // エラーを通知(例外を発生)
    return x / y;
  }
};

int main()
{
  try {
    Div::exe(1, 0);
  }catch ( Div::zero_div ){ // zero_div例外時のエラー処理
    std::cout << "0では割れません.\n";
  }
  return 0;
}

C++では,例外をオブジェクトの型で表現している.エラーを検知したコードはオブジェクトをスロー(送出)することで,例外を通知できる.スローするには,throw演算子を使用する.ユーザはtry-block文によって通知を受け取り,エラーの種類に応じた処理を行うことができる.例外処理を行うコードはハンドラと呼ばれる.

例外処理

例外の通知については,「例外スロー」で説明し,ここでは例外通知の受け取りと例外ハンドラについて説明する. try-blockの文法は,次の通りである.

try-block
  • try compound-statement handler-list
handler-list
  • handler handler-listopt
handler
  • catch ( exception-declaration ) compound-statement
exception-declaration
  • type-specifier-list declarator
  • type-specifier-list
  • type-specifier-list abstract-declarator
  • ...
補足
  • type-specifier-listは型や型指定子の列.
  • abstract-declaratorは抽象宣言子.宣言子演算子とcv修飾子から成る(例↓).
    • int
    • signed long int
    • const double
  • declaratorは宣言子.識別子(名前)と宣言子演算子とcv修飾子から成る.
  • compound-statementは複合文.
catchで使用するexception-declarationは,次のものを指定する.
exception-declaration検知できる型(例外)
type-specifier-list declarator型.もしクラスであればその公開派生クラスも可.
type-specifier-list
type-specifier-list abstract-declarator型.もしクラスであればその公開派生クラスも可.throw演算子で使用した項(オブジェクト)を受け取る.
...全て
exception-declarationにconstを追加しても,受け取る例外の種類が増える訳ではない.この場合,受け取ったオブジェクトが変更できないことを示す.

ハンドラの優先順位は,try文の中のcatchの順番に従う.つまり,コードの先に書いたハンドラから順に試される.


struct UserDefErr { int i; };
struct ZeroDivErr : public UserDefErr { };
struct OverFlowErr : public UserDefErr { };
struct UnderFlowErr : public UserDefErr { };

int main()
{
  try {
    // 例外を送出するコード
  }catch( OverFlowErr e ){
    // OverFlowErrのエラーの処理
  }catch( UserDefErr e ){
    // ZeroDivErrとUnderFlowErr,UserDefErrらのエラーの処理
  }catch( ... ) {
    // 残り全てのエラーの処理
  }
  return 0;
}
throw演算子の項(オブジェクト)は,多重継承を用いたクラスを使用することで,いくつかのグループに属するエラーを表現できる.

なお,ハンドラもまた例外送出できる.その場合,それより外で定義されたハンドラで受け取ることができる.


  try {
    try {
      throw int();
    }catch(int){
      throw int();
    }
  }catch (int){
    std::cout << "catch\n";
  }

関数本文の例外キャッチ

関数の本文の例外をキャッチするためには,後述の関数定義の2つ目の構文によって定義すれば良い.

function-definition
function-try-body
  • try ctor-initializeropt function-body handler-list
補足
  • decl-specifier-listは記憶指定子や型,関数指定子,friend,typedefから成る列.
  • declaratorは宣言子.識別子(名前)と宣言子演算子とcv修飾子から成る
  • ctor-initializerはメンバ初期化リストを使用する際に記述.
以下に例を示す.

int main()
try
{
  // 本文
  return 0;
}catch(...){
  //
}
この例のようにmain関数に用いた場合,大域変数のコンストラクタやデストラクタが投げた例外を除く全ての例外をキャッチできる.

非局所的な静的オブジェクトの初期設定子がスローした時に制御を握るための唯一の手段は,後述するstd::set_unexpectedを用いること.これは大域変数の使用を極力避けた方が良い理由の1つである.

メンバの初期化時の例外取得

メンバ初期化リストを使用した場合,それらのコンストラクタの例外をキャッチするためには,関数の本文の例外をキャッチする構文を使えば良い.以下にサンプルを示す.


class Hoge // コンストラクタがエラーを起こすクラス
{
public:
  Hoge(){}
  Hoge(int i){ throw 12; }
  ~Hoge(){}
};

class Fuga
{
public:
  Hoge x;
  int y;
  
  Fuga();
  ~Fuga(){}
};

Fuga::Fuga()
try
 : x(10), y(20)
{ 
  std::cout << "constructor body\n";
}catch(int i){
  std::cout << i << std::endl;
}

int main()
{
  Fuga f;
  return 0;
}
メンバ初期化リストで使用されるコンストラクタはこのようにして例外をキャッチすることができ,これ以外のコンストラクタを使った初期化や型変換の場合,「例外処理」で示した構文を,使えば良い. コンストラクタやデストラクタは値を返さないので,エラー報告をする手段が例外処理機構の存在は便利かもしれないが,コンストラクタ内での例外送出は,極力避けるようにするべきである.

キャッチされない例外

例外がキャッチされない場合,std::terminateが呼び出される.関数terminateは関数std::set_terminateによって振る舞いを変更できる.関数set_terminateは次のように宣言される.

set_terminate

terminate_handler set_terminate(terminate_handler) throw();
terminate_handler

typedef void (*terminate_handler) ();
関数terminateは,標準では関数abort,関数set_terminateである関数をセットした場合はその関数を呼び出す.なお,関数set_terminateの戻り値は,直前の関数set_terminateの呼び出しの際に渡された関数(または初期に設定された関数)のアドレスである.設定したハンドラは,制御を返さないことが前提となっている.制御を呼び出し側コードに返す場合,関数terminateは,関数abortを呼び出す.関数abortは,プログラムが異常終了したことを表す.それゆえ,正常終了したことを表す場合,関数exitを使用する.

例外がキャッチされなかったためにプログラムを終了するとき,デストラクタが呼び出されるかは言語処理系に依存する.

例外スロー

throw-expressin
throw expressionopt

例外の変換

exception-specificationにstd::bad_exceptionが含む時,unexpected()は,プログラムを終了する代わりに,std::bad_exceptionをスローする(私の環境ではそうならなかったが).std::bad_exceptionは標準ライブラリで定義されたクラスで,これを使用した場合,どの例外が問題を起こしたかという情報が失われる.

例外スローの制限

ある関数が送出する可能性のある例外は,関数宣言で指定できる.

declarator
  • declarator ( argument-declaration-list ) cv-qualifier-listopt exception-specificationopt
  • declarator-id
  • ptr-operator declarator
  • declarator [ constant-expressionopt ]
  • (declarator )
abstract-declarator
  • abstract-declaratoropt ( argument-declaration-list ) cv-qualifier-listopt exception-specificationopt
  • ptr-operator abstract-declaratoropt
  • abstract-declaratoropt [ constant-expressionopt ]
  • ( abstract-declarator )
exception-specification
  • throw ( type-id-listopt )
type-id-list
  • type-id
  • type-id-list type-id
type-id
  • type-specifier-list abstract-declaratoropt
補足
  • type-specifier-listは型や型指定子の列.
  • argument-declaration-listは0個以上の引数列.
  • argument-declaration-listは0個以上のconstやvolatileから成る列.
  • constant-expressionは定数式.
これを用いれば,ユーザは宣言を見るだけで関数定義の本文が分からなくてもエラーの種類を知ることができる.exception-specificationが省略された時,その関数は全ての例外を送出する可能性があることを意味する.また,type-id-listに1つの型も記述しなかった関数は,例外を発生させないことを表す.type-idにクラスが指定された場合,その派生クラスもまた送出し得る.以下に使用例を記す.

namespace User
{
  class Err1 {};
  class Err2 {};
  class Err3 {};
  class Hoge
  {
  public:
    void func(int i) throw(Err1, Err2);
  };
};

void User::Hoge::func (int i) throw(Err1, Err2)
{
  if (i==0){
    throw Err1();
  }else if (i < -1){
    throw Err2();
  }
  return;
}

int main()
{
  User::Hoge h;
  try {
    h.func(0);
  }catch ( User::Err1 ){
    std::cout << "err1\n";
  }catch (...){
    std::cout << "etc\n";
  }
  return 0;
}
User::Hoge::funcは,Err1かErr2の例外しか送出しない.それゆえ,ユーザはこれらのためのエラー処理だけ書けば良い.もし,関数製作者がこの宣言を破り他の種類の例外を送出した場合,実行時にstd::unexceptedが呼び出される.この関数は,デフォルトではstd::terminate()と同じ働きをする.std::terminateは通常abort()呼び出す.

関数の上書き(オーバーライド)

「ある関数と比べて別の関数の送出可能な例外の種類が多い場合のこと」を「制限が緩い」と表現する.関数の上書き(オーバーライド)を行いたい時,例外の制限が緩い関数は,それより制限が厳しい仮想関数を上書きできない.


class Err1 {};
class Err2 {};

class Base {
public:
	virtual void func1() throw(Err1){}
	virtual void func2() throw(Err1){}
	virtual void func3() throw(Err1){}
	virtual void func4() throw(Err1){}
	virtual void func5() throw(Err1){}
};

class Sub : public Base
{
public:
	void func1() {} 			// NG 制限が緩い(送出する例外の種類が多い)
	void func2() throw(Err1, Err2) {}	// NG 制限が緩い(送出する例外の種類が多い)
	void func3() throw(Err2) {}		// NG 制限が異なる
	void func4() throw(Err1) {}		// OK 制限が同じ(送出する例外の種類が同じ)
	void func5() throw() {}			// OK 制限が厳しい(送出する例外の種類が少ない)
};
この性質は,アップキャストした場合を考えると覚えやすい.これができてしまった場合,元の関数では発生しないはずの例外が発生してしまう.

関数ポインタ

関数の上書き(オーバーライド)の性質と同様に,例外の制限が厳しい関数のポインタに,例外の制限の緩い関数のアドレスを代入できない.


class Err1 {};
class Err2 {};

void func1 () {}
void func2 () throw(Err1, Err2) {}
void func3 () throw(Err2) {}
void func4 () throw(Err1) {}
void func5 () throw() {}

int main()
{
  void (*p)() throw(Err1);
  p = func1; // NG pの方が制限が厳しい
  p = func2; // NG pの方が制限が厳しい
  p = func3; // NG 制限が異なる
  p = func4; // OK pと制限が同じ
  p = func5; // OK pの方が制限が緩い
  return 0;
}
私の環境では無事コンパイルできてしまったが.

例外のユーザ定義変換

関数std::set_unexpectedによって,unexpectedが呼び出す関数を変更できる.std::set_unexpectedは,名前空間stdで次のように宣言される.

set_unexpected

unexpected_handler set_unexpected(unexpected_handler) throw();
unexpected_handler

typedef void (*unexpected_handler) ();
この関数は,ヘッダexceptionで定義される.

class Err1 {};

void dummy()  throw(Err1) { std::cout << "hoge\n"; }
void func() throw() {  throw Err1();}

int main()
{
  std::set_unexpected(err);
  try {
    func();
  }catch (Err1){
    std::cout << "err1\n";
  }
  return 0;
}

標準で定義された例外

標準ライブラリで定義された全ての例外は,std::exceptionクラス(ヘッダexceptionで宣言)の派生クラスである.それゆえ,関数で送出し得る例外を指定する場合や例外をキャッチする場合,このクラスを指定することで,標準ライブラリの全ての例外をまとめて扱うことができる.以下に,標準ライブラリで定義されたいくつかの例外をいくつか示す.

例外名(クラス名)例外が発生する関数・演算子宣言されるヘッダスローするもの
bad_allocnewnew言語
bad_castdynamic_casttypeinfo言語
bad_typeidtypeidtypeinfo言語
bad_exceptionexception-specificationexceptoin言語
out_of_rangeat(),bitset<>::operator[]stdexcept標準ライブラリ
invalid_argumentbitsetコンストラクタstdexcept標準ライブラリ
overflow_errorbitset<>::to_ulong()stdexcept標準ライブラリ
ios_base::failureios_base::clear()ios標準ライブラリ

その他

デストラクタ

エラー処理は,例外処理機構よりも先にデストラクタを考えた方が良い.デストラクタは,そのオブジェクトが破壊される際に必ず呼び出される.それゆえ,正常終了であろうと例外による終了であろうと必ず終了のための処理を行うことができる.この特性は,例外処理や平常時の終了処理をそれぞれ記述するよりも簡潔にし,エラーを少なくする. デストラクタの詳細については,「デストラクタ」で説明する.

例えば有限な資源を扱うクラス,ここではファイルの入出力を行うクラスを定義しようとした場合の例を示す.この時,デストラクタによってファイルをクローズすれば,ユーザが通常のクローズとエラー時のクローズを記述しなくても,必ずクローズされる.

typedef

exception-specificationは関数の型の一部ではない.それゆえtypedef文に含まれていてはならない.


// 引数なしと返り値なしの関数のポインタ型にfunctionという別名を付ける
typedef void (*function)(); 		// OK
typedef void (*function)() throw(); 	// NG : throw()は不要

制御構造としての例外処理

例外処理はその特性上,制御構文の一種として扱うことができる.以下に例を示す.


int main(){
  try {
    while (1)
     while(1)
      throw int();
  }catch (...){
    ;
  }
  std::cout << "hoge\n";
  return 0;
}
このようなコードはいくつかの場合で便利だが,使いすぎた場合,コードを分かり難くする要因となる.

目録

キーワード

-緑の文字をクリック-