例外とは,エラー処理からエラー報告を分離するための仕組み.このような仕組みがない場合,ライブラリの開発者は実行時にエラーを検出できるが,それをどのように処理したら良いか判断できない.逆にライブラリのユーザはそのようなエラーをどのように対処すれば良いか判断できるが,エラーを検出できない.
例えばある整数を別の整数で割って結果の整数を返す関数を定義しようとする.この時,数学的にゼロで除算することはできない(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;
}
詳細は後述するが,前述のコードに例外処理機構を追加すると次のようになる.なお,例外の種類と関数を関連付けるために名前空間を使用している.
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は複合文.
exception-declaration | 検知できる型(例外) |
---|---|
type-specifier-list declarator | 型.もしクラスであればその公開派生クラスも可. |
type-specifier-list | |
type-specifier-list abstract-declarator | 型.もしクラスであればその公開派生クラスも可.throw演算子で使用した項(オブジェクト)を受け取る. |
... | 全て |
ハンドラの優先順位は,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;
}
なお,ハンドラもまた例外送出できる.その場合,それより外で定義されたハンドラで受け取ることができる.
try {
try {
throw int();
}catch(int){
throw int();
}
}catch (int){
std::cout << "catch\n";
}
関数本文の例外キャッチ
関数の本文の例外をキャッチするためには,後述の関数定義の2つ目の構文によって定義すれば良い.
- function-definition
- decl-specifier-listopt declarator ctor-initializeropt function-body
- decl-specifier-listopt declarator function-try-body
- 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(...){
//
}
非局所的な静的オブジェクトの初期設定子がスローした時に制御を握るための唯一の手段は,後述する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) ();
例外がキャッチされなかったためにプログラムを終了するとき,デストラクタが呼び出されるかは言語処理系に依存する.
例外スロー
- 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は定数式.
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;
}
関数の上書き(オーバーライド)
「ある関数と比べて別の関数の送出可能な例外の種類が多い場合のこと」を「制限が緩い」と表現する.関数の上書き(オーバーライド)を行いたい時,例外の制限が緩い関数は,それより制限が厳しい仮想関数を上書きできない.
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) ();
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_alloc | new | new | 言語 |
bad_cast | dynamic_cast | typeinfo | 言語 |
bad_typeid | typeid | typeinfo | 言語 |
bad_exception | exception-specification | exceptoin | 言語 |
out_of_range | at(),bitset<>::operator[] | stdexcept | 標準ライブラリ |
invalid_argument | bitsetコンストラクタ | stdexcept | 標準ライブラリ |
overflow_error | bitset<>::to_ulong() | stdexcept | 標準ライブラリ |
ios_base::failure | ios_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;
}