10.1 例外
この項では例外という概念について学びます。
さて、例外は平たく言えば想定外の自体が発生した場合の処理です。 C言語ではある関数内でエラーが発生した場合、プログラマが独自的に決めた値などを戻り値にしてその内部でエラーが起きた事を知らせようというようなプログラムがありましたが、プログラマが独自的に決めたルールというのは言語規約ではないので、いつでも破る事ができます。それは困りますよね。 C++でも、戻り値で判定を行う文化はしばしば残っていますが、より重大な内部エラーを孕んでしまった場合は、例外を送出する事が多いです。 この項では、そのように送出された例外の受け取り方と、逆に例外の送出を行う方法、またそれに関連する内容を説明します。
10.1.1 try,throw,catch
例外は、主にtry
、throw
、catch
の三つのキーワードで構成されます。例外が発生する可能性のある部分をtry
ステートメントで囲み、その後のcatch
ステートメントで受け取る事ができます。例外の送出はthrow
キーワードで行います。まずは簡単なサンプルを見て見ましょう。
#include<iostream>
struct Exception{
const char* what()const noexcept{return "Sended Except";}
};
int main()
{
try{ // 例外が発生する可能性のある構文を囲む
throw Exception(); // 例外を発生させる
}catch(const Exception& exp){ // 送出させた例外を受け取る
std::cerr<<exp.what()<<std::endl;
}
}
実行結果は以下の通りです。
Sended Except
上記のコードではtry
ステートメントの内部で必ずException
が例外送出されるため全く意味はありませんが、基本的な構文や使い方は上記の通りです。throw
では、上記の通り例外を表現するrvalueオブジェクトを指定します。catch
の仮引数リストでは、送出されると思われる例外型を記述します。
またtry
~catch
は、関数内部のスコープに限らず関数の外側に設置することも可能です。
int main()try
{
// ...
}catch(const Exception& exp){
//...
}
ところで、例外がthrow
された時、catch
を行わなかった場合どうなるのでしょうか。
int main()
{
throw 1;
}
この場合、コンパイルには成功しますが、実行すると異常終了します。異常終了の際のエラー内容は環境によって様々なので一概に断定する事はできませんが、筆者の環境では以下のように出力され、異常終了しました。
terminate called after throwing an instance of 'int'
zsh: abort ./a.out
よって、正常に動かすためには例外を必ずcatch
しなければならないという事になりますが、その用途によって利用方法は様々であると言えるでしょう。
10.1.2 標準で用意された例外型
例外クラスは自身でオリジナルのものを定義する事は勿論できますが、標準に例外クラスが用意されていたりもします。それらは<stdexcept>
ヘッダに定義されており、実行時エラーと論理エラーの二つのエラーモデルを表現しています。二つのエラーモデルの意味合いとしては以下のようになっています。
- 論理エラー
- プログラムが論理的に誤っている事に起因するエラー。理論的には、それらはコンパイルの前に予め避けることができる。
- 実行時エラー
- コンパイル時に検出する事ができないエラー。
標準で用意された例外型は、主に標準で用意された他ライブラリや組み込みから用いられますが、勿論ユーザーが利用する事も可能です。
クラス名 | 意味 | エラーモデル |
---|---|---|
logic_error | 論理エラーを示す | 論理 |
domain_error | 定義域エラーを示す | 論理 |
invalid_argument | 不正な引数を示す | 論理 |
length_error | 長すぎるオブジェクトを作ろうとしたことを示す | 論理 |
out_of_range | 引数が許容範囲外であることを示す | 論理 |
runtime_error | 実行時エラーを示す | 実行時 |
range_error | 値が範囲外になったことを示す | 実行時 |
overflow_error | 数値計算の結果がオーバーフローしたことを示す | 実行時 |
underflow_error | 数値計算の結果がアンダーフローしたことを示す | 実行時 |
これらは全てコンストラクタに文字列を受け取り、what
という仮想メンバ関数を保持しています。what
はコンストラクタで設定した文字列を返します。仮想メンバ関数となっている理由は、そのメンバ関数をオーバーライドできるようにするためです。
以下にサンプルコードを示します。
#include<stdexcept>
#include<iostream>
struct my_exception:std::logic_error{
using std::logic_error::logic_error;
const char* what()const noexcept override
{
return std::logic_error::what();
}
};
int main()
{
try{
throw std::logic_error("logic_error exception");
}catch(const std::logic_error& le){
std::cerr<<le.what()<<std::endl;
}
try{
throw my_exception("my_exception exception");
}catch(const my_exception& me){
std::cerr<<me.what()<<std::endl;
}
}
実行結果は以下の通りです。
logic_error exception
my_exception exception
10.1.3 noexceptキーワード
noexcept
は例外に関する情報を制御する構文です。このキーワードは例外仕様と、演算子としての二つの意味があります。二つの意味の使い分けは記述した部位によって区別されます。
例外仕様
例外仕様とは、その関数が例外を送出する可能性があるか否かを制御するものです。
例外仕様としてのnoexcept
は以下のように記述する事ができます。
void f()noexcept;
void f()noexcept( /* bool値に変換可能な整数定数式 */ );
noexcept
もしくは、noexcept()
の評価結果noexcept(true)
となる場合、その関数からは例外が送出してはなりません。もし例外が送出された場合、std::terminate
関数によってプログラムは異常終了します。よって、noexcept
またはnoexcept()
の結果noexcept(true)
となる関数は例外が送出されない関数である事を示す事ができます。
逆にnoexcept
例外仕様に対してその評価がfalse
に評価される整数定数式を指定した関数は、あらゆる例外を送出する可能性がある事を明示します。
尚、noexcept例外仕様を指定しない関数は、一部の例外を除いて、デフォルトでnoexcept(false)
を意味しますが、デストラクタとdelete演算子は、明示的にnoexcept(false)
に評価される整数定数式を指定しない限り、デフォルトでnoexcept(true)
となります。
void f()noexcept; // 関数fは例外を送出しない
void g()noexcept(true); // 関数gは例外を送出しない
void h()noexcept(false); // 関数hは例外を送出する可能性がある
演算子
演算子としてのnoexcept
は、引数に指定した定数式が、例外を送出する可能性があるかをコンパイル時に判定し、bool
値を返します。演算子としてのnoexcept
は、例外仕様としてnoexcept(false)
と評価される関数(関数ポインタを含む)を指定した場合、false
が返されます。
void f()noexcept; // 例外を送出しない
void g()noexcept(false); // 例外を送出する可能性がある
int main()
{
static_assert(noexcept(f())); // true
static_assert(!noexcept(g())); // !false
}
この演算子は、関数が宣言のみで実装がないのにも関わらずnoexcept
に指定できている事から分かる通り、sizeof
、decltype
と同じく引数として指定された式は、実行時評価はされません。
これらの両機能を利用して、例えばあるconstexpr
でない一文が例外を送出しない場合、その関数自体も例外を送出しない事を示すという使い方ができます。
void f()noexcept{}
void g()noexcept(noexcept(f())){} // 内包されたnoexceptは演算子として、その外側のnoexceptは例外仕様としてのnoexcept
このように記述した場合、関数f
がnoexcept
である場合に関数g
はnoexcept(true)
に、そうでない場合はnoexcept(false)
になります。
void f()noexcept{}
void g()noexcept(false){}
void h()noexcept(noexcept(f())){f();}
void i()noexcept(noexcept(g())){g();}
int main()
{
static_assert( noexcept(h()) ); // noexcept(true) == noexcept(h())
static_assert( !noexcept(i()) ); // noexcept(false) == noexcept(i())
}
10.1.4 noexceptは関数型に含まれる
例えば以下のように関数のアドレスを格納しようとした場合、以下のように記述する事が望ましいです。
void f()noexcept{};
int main()
{
void (*f_ptr)()noexcept=f;
}
何が望ましい部分なのかというと、関数ポインタの宣言部分です。
関数ポインタの型はvoid (*f_ptr)()noexcept
となっています。そうです、noexcept
は型の一部なのです。
実は、以下のように関数ポインタ側ではnoexcept
指定をしなくても規格違反ではありません。
void f()noexcept{};
int main()
{
void (*f_ptr)()=f; // 例外を投げない関数fの関数ポインタをnoexceptにしなくても良い
}
しかし、ポイントしている関数がnoexcept
であるのならば、最適化や明示性などを考えると関数ポインタ側にもnoexcept
を付与する事が望ましいです。
尚、noexcept
でない関数をポイントする型を、noexcept
指定する事はできません。
void f(){};
int main()
{
void (*f_ptr)()noexcept=f; // エラー!例外を投げるかもしれない関数fのアドレスをnoexceptな関数のポインターで格納できない
}
この関数の型にnoexcept
が含まれるという機能は、割と最近に組み込まれた機能なので、対応していないコンパイラに遭遇する可能性もまだまだあるでしょう。
そこで、以下の標準で定義されるマクロを用いる事で正しくこの機能が搭載されているか知る事ができます。
#ifdef __cpp_noexcept_function_type
// noexceptが関数の型の一部でなければならないコード
#else
// そうでないコード...
#endif
尚、__cpp_noexcept_function_type
の値は201510
です。
10.1.5 標準ライブラリによるサポート
<exception>
ヘッダーをインクルードする事で例外の扱いに関連したいくつかのクラスと関数を利用する事ができます。
#include <exception>
このヘッダーから提供される各機能について見ていきます。まず、std::exception
クラスについてです。このクラスは標準ライブラリが提供する全ての例外クラスの基本クラスとなるクラスであり、従って標準の例外は全てこのクラスでcatch
する事ができます。
try{
throw std::runtime_error("example");
}catch(const std::exception&){
// ....
}
次はstd::bad_exception
クラスについてです。これも例外クラスですが、このクラスについては殆ど言うことがありません。なぜならば、これは C++17 よりも前のバージョンまで非推奨として存在していたstd::set_unexpected
関数に対して利用するためのもののような存在だったからです。ただこの例外クラスが投げられる事も全くなくはありません。例えば後に説明するstd::current_exception
関数で得た例外オブジェクトのコピーコンストラクタが例外を投げる時にこの例外クラスは投げられます。
nested_exception
、set_terminate
、get_terminate
、terminate
、uncaught_exception
、exception_ptr
、current_exception
、rethrow_exception
、make_exception_ptr
、throw_with_nested
、rethrow_if_nested