C++/例外処理
例外処理
[編集]例外処理(れいがいしょり、exception handling)とは、プログラム実行中に予期せぬ異常が発生した場合に、通常の実行フローを中断し、エラーメッセージの表示や適切な処理を行うプログラムの手法です。
C++の例外処理
[編集]C++における例外処理は、try
, catch
, throw
, noexcept
のキーワードを使用して行われます。例外が発生する可能性のあるコードをtry
ブロック内に配置し、例外が発生した場合の処理をcatch
ブロックでキャッチします。throw
キーワードを使用して、例外を手動で発生させることもできます。また、C++にはfinally
ブロックは存在しないため、例外が発生したかどうかに関わらず必ず実行されるコードを別途記述する必要があります。
例を見てみましょう:
- コード例
#include <iostream> void process(int value) { try { if (value < 0) { throw "値が負です"; // 文字列を例外としてスロー } std::cout << "処理された値: " << value << std::endl; } catch (const char *errorMsg) { std::cout << "例外がキャッチされました: " << errorMsg << std::endl; } } int main() { process(10); // 正の値を処理 process(-5); // 負の値を処理(例外が発生する) return 0; }
- 実行結果
処理された値: 10 例外がキャッチされました: 値が負です
この例では、process
関数が負の値を受け取った場合に例外をスローし、catch
ブロックでその例外をキャッチしてメッセージを表示しています。
ただし、C++ではさまざまな型の例外をキャッチすることができます。catch
ブロック内でint
、double
、std::exception
など、適切な型を指定してキャッチすることができます。また、複数のcatch
ブロックを使用して、異なる種類の例外を個別に処理することも可能です。
std::exception
[編集]std::exception
は、C++標準ライブラリで定義されている基本的な例外クラスです。std::exception
は、例外の基底クラスとして機能し、他の例外クラスの基準となります。
std::exception
は、以下のメンバ関数を持っています:
what()
:例外に関する説明を返すためのメソッドです。このメソッドをオーバーライドして、具体的な例外の説明を提供することができます。通常、このメソッドは文字列を返します。
例えば、カスタム例外クラスを作成する際に、std::exception
を継承し、what()
メソッドをオーバーライドすることが一般的です。こうすることで、例外が発生した際に例外の詳細を提供できます。
以下は、std::exception
を継承してカスタム例外クラスを作成する例です:
- コード例
#include <exception> #include <iostream> class CustomException : public std::exception { public: virtual const char *what() const noexcept override { return "Custom exception occurred"; } }; int main() { try { throw CustomException(); } catch (const std::exception &E) { std::cout << "Caught exception: " << E.what() << std::endl; } return 0; }
- 実行結果
Caught exception: Custom exception occurred
この例では、CustomException
クラスがstd::exception
を継承し、what()
メソッドをオーバーライドしています。main()
関数でこの例外を投げ、catch
ブロックで例外をキャッチして、その詳細を表示しています。
C++ではゼロ除算が起こっても例外はthrowされない
[編集]C++の整数除算において、ゼロで割ることはプログラムをクラッシュさせるランタイムエラーを引き起こしますが、明示的に例外をスローするわけではありません。C++における整数除算によるゼロ除算は未定義動作(undefined behavior)であり、その結果は規定されていません。このため、プログラムがクラッシュするか、予期せぬ結果が生じるかは確実ではありません。
しかし、C++では浮動小数点数の場合、ゼロ除算が特別な値("infinity" や "NaN")を生成することがあります。しかし、これも例外をスローするわけではなく、特殊な浮動小数点値を返します。
例外をスローしてゼロ除算を処理したい場合、例外を発生させる独自の関数を作成することができます。たとえば、次のような関数を作成してゼロ除算を処理できます:
- コード例
#include <iostream> #include <stdexcept> double divideSafely(double numerator, double denominator) { if (denominator == 0) { throw std::runtime_error("Division by zero"); } return numerator / denominator; } int main() { try { double result = divideSafely(10.0, 0.0); std::cout << "Result: " << result << std::endl; } catch (const std::exception &e) { std::cout << "Exception caught: " << e.what() << std::endl; } return 0; }
- 実行結果
Caught exception: Custom exception occurred
この例では、divideSafely
関数がゼロ除算をチェックし、ゼロで割ることがあればstd::runtime_error
をスローします。main()
関数ではこの関数を呼び出し、例外をキャッチしてメッセージを表示します。
例外に対応した数値クラスを定義する
[編集]SafeDouble
クラスを定義して、割り算演算子をオーバーロードし、例外に対応する方法を示します。このクラスでは、ゼロで割ることを検出して例外をスローするようにします。
- コード例
#include <iostream> #include <stdexcept> class SafeDouble { private: double value; public: SafeDouble(double val) : value(val) {} double getValue() const { return value; } friend SafeDouble operator/(const SafeDouble &num, const SafeDouble &denom) { if (denom.value == 0) { throw std::runtime_error("Division by zero"); } return SafeDouble(num.value / denom.value); } }; int main() { try { auto a = SafeDouble(10.0); auto b = SafeDouble(0.0); SafeDouble result = a / b; std::cout << "Result: " << result.getValue() << std::endl; } catch (const std::exception &e) { std::cout << "Exception caught: " << e.what() << std::endl; } return 0; }
- 実行結果
Exception caught: Division by zero
SafeDouble
クラスでは、value
というメンバー変数を持ち、割り算演算子をフレンド関数として定義しています。この演算子では、ゼロ除算が検出されると例外 std::runtime_error
をスローし、そうでなければ安全な割り算を実行します。main()
関数では、SafeDouble
オブジェクトを作成し、割り算を行って例外をキャッチして表示します。
多態的例外
[編集]C++における多態的例外 (Polymorphic Exception) とは、異なる型の例外をキャッチするために基底クラスと派生クラスを使用する例外処理の機能です。
通常、C++の例外処理は throw
を使って例外を発生させ、try
ブロックで例外をキャッチします。多態的例外は、例外を表すクラスの階層構造を作成し、基底クラスのポインタや参照を使用して、異なる型の例外をキャッチすることを可能にします。
#include <exception> #include <iostream> #include <sstream> #include <vector> // 算術エラーの基底クラス class ArithmeticError : public std::exception { public: virtual const char *what() const noexcept override { return "Arithmetic error occurred"; } }; // ドメインエラー class DomainError : public ArithmeticError { public: const char *what() const noexcept override { return "Domain error occurred: Cannot divide zero by zero in this domain"; } }; // 0で割り算エラー class DivideByZero : public ArithmeticError { public: const char *what() const noexcept override { return "Divide by zero error occurred: Cannot divide by zero"; } }; class SafeInteger { private: int value; public: SafeInteger(int val) : value(val) {} // 割り算演算子のオーバーロード auto operator/(const SafeInteger &other) const { if (value == 0 && other.value == 0) { throw DomainError(); } if (other.value == 0) { throw DivideByZero(); } return SafeInteger(value / other.value); } // 文字列化メソッド std::string toString() const { std::stringstream ss; ss << value; return ss.str(); } }; // SafeInteger オブジェクトを cout に直接出力できるようにする std::ostream& operator<<(std::ostream& os, const SafeInteger& si) { os << si.toString(); return os; } int main() { auto zero = SafeInteger(0); auto one = SafeInteger(1); std::vector<std::pair<SafeInteger, SafeInteger>> divisions = { {one, one}, {zero, one}, {one, zero}, {zero, zero} }; for (const auto& division : divisions) { try { std::cout << "Result: " << division.first << " / " << division.second << " = " << (division.first / division.second) << std::endl; } catch (const ArithmeticError &e) { std::cout << "Caught Exception: " << e.what() << std::endl; } } return 0; }
このC++のコードでは、ArithmeticError
を基底クラスとして、DomainError
と DivideByZero
の2つの例外クラスを派生させています。
try
ブロック内で catch
が基底クラスのポインタ const ArithmeticError& e
を受け取ることで、異なる型の例外をキャッチし、処理することができます。これにより、派生クラスの例外を基底クラス型で一括して処理することが可能で、かつライブラリなどが発するほかの例外とは弁別できます。
- 多態的例外のメリット
多態的例外のメリットはいくつかあります。
- 柔軟性と拡張性: 多態的例外は、複数の異なる例外を同じ基底クラスでキャッチすることができます。これにより、異なる種類の例外をまとめて処理することができ、コードの柔軟性と拡張性を高めます。新しい例外を追加する際に、既存の例外ハンドリングコードを変更する必要がなくなります。
- 抽象化と分離: 基底クラスを使用することで、例外の発生元と処理を分離し、より抽象的に扱うことができます。例外の詳細な実装が変更された場合でも、基底クラスのインターフェースを変更することなく、既存の例外処理コードを変更せずに済みます。
- 階層構造の活用: クラスの継承関係を利用することで、例外を階層化して管理することができます。これにより、異なるレベルのエラーをより細かく扱うことができます。たとえば、異なるエラータイプに基づいて異なる処理を行うことができます。
- 保守性の向上: 多態的例外は、コードの保守性を向上させます。同じ基底クラスを継承した例外をグループ化し、それらを一元管理することができます。これにより、コード全体での例外処理の一貫性を保ちやすくなります。
- 読みやすいコード: 同じ基底クラスの例外をキャッチするコードは、読みやすく、理解しやすいです。特定の例外タイプに依存しないより一般的な例外処理が可能となります。
ただし、例外処理の設計においては適切なバランスが重要です。過度な例外の階層化や、過剰な一般化はコードの複雑性を増やす可能性があります。常にコードの保守性と可読性を考慮しながら、適切なレベルの抽象化を行うことが重要です。
noexcept
[編集]noexcept
は、C++11から導入されたキーワードで、関数が例外を送出しないことを宣言するために使用されます。これはコンパイラに対して、その関数が例外を投げないことを保証することを示すものです。
noexcept
キーワードは、次のように使用されます:
- 関数が例外をスローしないことを示す:
void func() noexcept { // 例外をスローしないコード }
- 関数が特定の条件下で例外をスローしないことを示す:
void func() noexcept(true /* or false */) { // true なら例外をスローしないことを示す // false なら例外をスローする可能性があることを示す }
- 関数テンプレートに対して
noexcept
を使用する:template <typename T> void func() noexcept(noexcept(T())) { // T()の例外仕様に基づいて、この関数が例外をスローするかどうかを推論 }
noexcept
を使用することで、コンパイラは最適化を行う際に例外の処理を省略することができ、プログラムのパフォーマンスを向上させることができます。
しかし、noexcept
指定された関数が例外を送出した場合は、std::terminate
が呼ばれるため、プログラムは強制終了します。そのため、noexcept
を使う際には、その関数が本当に例外を送出しないことを確認することが重要です。
std::exception
: これは、C++標準ライブラリで提供される基本クラスで、ほとんどの標準例外クラスの基底クラスです。他の例外クラスは通常、このクラスを継承しています。std::nothrow
: これは、new
演算子が失敗した場合に例外をスローしないようにするためのオプションです。new(std::nothrow)
と記述することで、nullptr
を返すことができます。std::terminate
: 例外がキャッチされない場合や、予期しない例外が発生した場合に呼び出される関数です。通常、この関数はプログラムを終了させますが、カスタムのstd::terminate_handler
を設定することで挙動をカスタマイズできます。std::unexpected
:throw
された例外がthrow
仕様と一致しない場合に呼び出される関数です。通常、これはstd::terminate
を呼び出しますが、std::set_unexpected
を使用してカスタムのstd::unexpected_handler
を設定することができます。
これらのキーワードや標準ライブラリのクラスは、例外処理や例外安全性の管理に重要な役割を果たします。例外処理に関連する標準ライブラリの機能を活用することで、より堅牢で安全なコードを記述することができます。
- C++の標準ライブラリがthrowする例外
C++の標準ライブラリは、さまざまな状況で例外をスローします。以下は、いくつかの主要な例外の種類とその状況です。
std::bad_alloc
: メモリの割り当てに失敗した場合にスローされます。通常はnew
演算子がstd::nothrow
で修飾されていない場合に発生します。std::out_of_range
: インデックスや範囲が有効でない場合に、std::vector
やstd::array
などのコンテナからスローされることがあります。std::logic_error
とstd::runtime_error
: これらは標準例外クラスの基底クラスです。std::logic_error
はプログラムの論理エラーを表し、std::runtime_error
は実行時エラーを表します。std::invalid_argument
: 関数に渡された引数が無効な場合にスローされることがあります。std::length_error
: コンテナのサイズが許容される最大サイズを超えた場合にスローされることがあります。std::ios_base::failure
: 入出力操作が失敗した場合に、std::fstream
やstd::stringstream
などのストリームからスローされます。std::bad_function_call
: 空のstd::function
オブジェクトに対して呼び出しが行われた場合にスローされます。
例外仕様の推論
[編集]C++における例外仕様の推論(Exception Specification Inference)は、関数が例外を投げる可能性をコンパイラが静的に推測するプロセスです。
noexcept
キーワードを使った関数宣言や、関数内で発生する可能性のある例外を推論するために使われます。例外仕様の推論にはいくつかの場面があります。
noexcept
仕様の推論
[編集]noexcept
キーワードを使った関数宣言で、明示的に関数が例外を投げないことを宣言できます。また、コンパイラは関数内の処理を見て、暗黙的にnoexcept
の有無を推測します。
void foo() noexcept { // 例外を投げないことが保証された処理 } void bar() { // 何らかの処理 throw SomeException(); // コンパイラはここで例外がスローされることを検知し、barはnoexceptでないことを推測する }
関数テンプレートの例外仕様の推論
[編集]関数テンプレートでは、テンプレート引数から関数内で発生する可能性のある例外を推測します。
template <typename T> void func() noexcept(noexcept(T())) { // T()の例外仕様に基づいて、この関数が例外をスローするかどうかを推論 }
式の例外仕様の推論
[編集]コンパイラは、式内で例外をスローする可能性を推論することもあります。例えば、throw
キーワードや特定の関数呼び出しの例外仕様を推測することがあります。
void foo() { int x = 0; int y = 10; int z = y / x; // ゼロ除算は例外をスローする可能性があるので、この式はnoexceptではないことを推測する }
例外仕様の推論は、コンパイラがコードを解析して、関数内で例外が発生する可能性を静的に判断するプロセスです。これにより、例外安全性の向上や、例外がどのように処理されるかをプログラマが予測できるようになります。
- C++03までの例外仕様(Dynamic Exception Specification
- 動的例外仕様(Dynamic Exception Specification): 関数が投げる可能性のある例外を関数のシグネチャで宣言する構文が存在しました。
void foo() throw(SomeExceptionType);
- 不完全な機能: この仕様は便利なように見えましたが、実際には多くの問題を引き起こしました。関数が投げる可能性のある例外を宣言するだけで、実際にスローする例外を管理することが難しかったり、コンパイラ最適化の障害になったりしました。
- C++11以降の変更
- 動的例外仕様の非推奨化: C++11では動的例外仕様が非推奨とされ、C++17では削除されました。
- noexcept仕様の導入:
noexcept
キーワードが導入され、関数が例外を投げないことを示すために使用されます。void foo() noexcept;
- 例外指定の推論:
noexcept
キーワードは関数の例外仕様を明示するだけでなく、関数が例外を投げるかどうかを推論するのにも利用されます。 - 強力なRAIIと例外安全性: C++11以降、RAII(Resource Acquisition Is Initialization)と例外安全性が重視され、リソース管理と例外処理の統合が強化されました。これにより、例外がスローされてもリソースのリークを避けることができます。
- 現在のC++の状況
- 例外仕様の削除: C++17で動的例外仕様が削除されました。現在では
noexcept
仕様を使用して、例外安全性を宣言することが推奨されています。 - 例外安全性の確保: RAII、スマートポインタ、例外仕様の推論などが、例外安全性を確保するための主要な手法となっています。
Javaのfinallyブロックの模倣
[編集]C++にはJavaのfinally
ブロックと同様の動作を実現する構文はありませんが、RAII(Resource Acquisition Is Initialization)というC++の概念があります。RAIIは、リソースの確保と解放をオブジェクトのライフサイクルに結びつける方法です。
一般的に、リソース(メモリ、ファイルハンドル、ロックなど)を取得した場合、それを解放するためにはそのスコープを抜ける時に解放処理を行う必要があります。これをRAIIを使って行うと、オブジェクトのデストラクタがリソースの解放を担当するため、スコープを抜けるときに自動的にリソースが解放されます。
例えば、ファイルハンドルを扱う場合、std::fstream
はRAIIの考え方を使用しています。ファイルを開くときにオブジェクトを作成し、そのオブジェクトがスコープを抜けるときに自動的にファイルが閉じられます。
#include <fstream> #include <iostream> void doSomethingWithFile() { std::fstream file("example.txt", std::ios::in | std::ios::out); if (!file.is_open()) { std::cerr << "Failed to open the file." << std::endl; return; } // ファイルを読み書きする処理 // 関数が終了するときに、file オブジェクトがスコープを抜けるため、 // 自動的にファイルが閉じられる(リソースの解放) }
もし、特定の処理をtry
ブロック内で行い、catch
ブロックの後に共通のクリーンアップ処理を実行したい場合、スコープ内でRAIIを使ったオブジェクトを活用してそれを行います。
#include <iostream> class MyResource { public: MyResource() { std::cout << "Resource acquired." << std::endl; } ~MyResource() { std::cout << "Resource released." << std::endl; } void doSomething() { // リソースを使った処理 } }; void doSomethingWithResource() { MyResource resource; try { // リソースを使った処理 resource.doSomething(); } catch (const std::exception &e) { std::cerr << "Exception occurred: " << e.what() << std::endl; // 例外が発生した場合の処理 } // 共通のクリーンアップ処理(finally ブロックに相当) }
このように、C++ではRAIIを使用してリソース管理を行い、オブジェクトのデストラクタを利用してリソースの解放を確実に行います。これにより、finally
ブロックのような共通の後始末処理を実現することができます。
std::shared_ptr
[編集]finally
ブロックのような振る舞いを模倣するには、std::shared_ptr
を使ってリソース管理を行う方法があります。リソース管理のためにRAIIを利用し、shared_ptr
のカスタムデリータ(custom deleter)を使用して特定の後始末処理を行います。
以下は、finally
ブロックに相当するものを模倣するための例です。
#include <iostream> #include <memory> // デリータとして後始末処理を行う関数 void cleanupFunction() { std::cout << "Finally block equivalent cleanup." << std::endl; // ここに後始末処理のコードを記述する } int main() { // shared_ptrのカスタムデリータに後始末処理を行う関数を設定する std::shared_ptr<int> resource(nullptr, [](int *ptr) { if (ptr) { cleanupFunction(); } delete ptr; }); try { // リソースの割り当て int *rawPtr = new int(42); resource.reset(rawPtr); // リソースを使った処理 std::cout << "Resource value: " << *resource << std::endl; } catch (const std::exception &e) { std::cerr << "Exception occurred: " << e.what() << std::endl; // 例外が発生した場合の処理 } // resourceのスコープを抜ける時に、カスタムデリータが後始末処理を行う return 0; }
このコードは、C++の標準ライブラリである<memory>ヘッダーを使用し、std::shared_ptrを使って後始末処理を実現する方法を示しています。
cleanupFunction()は、後始末処理を行う関数です。ここでは単純なメッセージを出力していますが、実際のアプリケーションではリソースの解放やクリーンアップなどを行うでしょう。
std::shared_ptrのカスタムデリータを使用して、リソース管理と後始末処理をカプセル化しています。このカスタムデリータは、リソースの解放とともにcleanupFunction()を呼び出して後始末処理を行います。nullptrとして初期化されたshared_ptrは、空のデリータ関数を持つことになります。
main()関数内では、リソースをint型のポインタで確保し、std::shared_ptrに割り当てています。これにより、リソースの所有権と管理がshared_ptrに移ります。
try-catchブロック内では、リソースの利用や可能な例外のキャッチが行われます。もし例外が発生した場合、catchブロックでエラーメッセージを出力することができます。
main()関数の最後で、shared_ptrのスコープを抜ける際にカスタムデリータが呼ばれ、後始末処理が実行されます。これにより、リソースが解放され、cleanupFunction()が呼ばれます。
この例では、shared_ptrのカスタムデリータを使ってRAIIの概念を応用し、リソースの自動管理と後始末処理を行っています。これにより、リソースの安全な管理と例外が発生した際の安全な後始末が実現されます。
std::unique_ptr
[編集]ラムダ式と例外処理を組み合わせることで、柔軟で効果的なエラーハンドリングや後始末処理を行うことができます。
include <functional> #include <iostream> void processResource() { std::cout << "Processing resource." << std::endl; throw std::runtime_error("An error occurred while processing."); } int main() { try { // ラムダ式を使って後始末処理を定義 auto cleanup = []() { std::cout << "Performing cleanup." << std::endl; // ここに後始末処理のコードを記述する }; // リソース処理をラムダ式で囲む [&]() { // 後始末処理をスコープを抜ける際に実行する std::unique_ptr<decltype(cleanup), std::function<void()>> guard( &cleanup, [](auto *ptr) { (*ptr)(); // 後始末処理を呼び出す }); // リソースの処理 processResource(); }(); } catch (const std::exception &e) { std::cerr << "Exception caught: " << e.what() << std::endl; } return 0; }
この例では、processResource()
という関数でエラーをスローし、それをメインのtry-catch
ブロックでキャッチしています。さらに、ラムダ式を使用して、リソース処理中に後始末処理を実行する方法を示しています。
ラムダ式は、[&](){ /* ラムダの中身 */ }
という形式で記述されます。このラムダ式は即時関数として使われ、リソース処理のスコープ内で実行されます。
ラムダ式内で、std::unique_ptr
を使用してカスタムデリータを持つスマートポインタを作成しています。これにより、リソース処理スコープを抜ける際に後始末処理が呼び出されるようになります。
エラーが発生すると、catch
ブロックが実行されて例外がキャッチされます。この方法を使用すると、例外処理と後始末処理を効果的に組み合わせることができます。
まとめ
[編集]C++の例外処理は、プログラム実行中に発生するエラーや異常状態に対処するためのメカニズムです。以下にC++の例外処理に関するまとめを示します。
例外の基本構造
[編集]- 例外のスロー:
throw
キーワードを使用して、プログラムのある箇所で例外をスローします。throw SomeException("Something went wrong!");
- 例外のキャッチ:
try-catch
ブロックを使用して、例外をキャッチし処理を行います。try { // 例外が発生する可能性のあるコード } catch (const SomeException& e) { // 例外が発生した時の処理 }
例外の型
[編集]- 標準ライブラリには、
std::exception
を基底とする様々な例外クラスが用意されています。カスタム例外クラスを定義することもできます。
RAIIと例外処理
[編集]- RAII(Resource Acquisition Is Initialization): リソースの解放をオブジェクトのライフサイクルに結びつけ、リソースリークを避けるための手法。
std::unique_ptr
やstd::shared_ptr
などのスマートポインタを利用して、自動的にリソースを解放することができます。
例外仕様(Exception Specification)
[編集]- C++において、古いバージョンの言語仕様である動的例外仕様がありましたが、C++17で非推奨とされ、C++20で削除されました。動的例外仕様では、関数が投げる可能性のある例外を指定しますが、使い勝手が悪く、推奨されなくなりました。
例外処理の注意点
[編集]- 例外安全性: リソースリークを避けるために、例外発生時にオブジェクトの破棄やリソースの解放を保証することが重要です。
- スタックアンウィンド: 例外がスローされると、スタックを巻き戻して適切なハンドラに渡すプロセスが発生します。このプロセスをスタックアンウィンドと呼びます。
例外処理のベストプラクティス
[編集]- 関数が例外を送出する可能性がある場合には、その旨をドキュメントに記述することが重要です。
- 複雑なリソース管理やクリーンアップを伴う場合、RAIIやスマートポインタを活用して例外安全性を確保することが望ましいです。
- 例外を適切にキャッチして処理することで、プログラムのロバスト性を高めることができますが、過度な例外処理はコードの読みやすさを損なう可能性があります。
以上が、C++における例外処理に関する基本的なまとめです。例外処理はプログラムの信頼性と保守性を高めるために重要な概念ですが、使い方には注意が必要です。
脚註
[編集]