[C++] 複数の例外をtry-catchで処理する方法

C++では、例外処理を行うためにtry-catchブロックを使用します。

複数の例外を処理する際には、catchブロックを複数用意することで、異なる型の例外を個別に処理することが可能です。

catchブロックは、特定の例外型に対応しており、例外が発生すると、最初に一致するcatchブロックが実行されます。

また、すべての例外をキャッチするために、catch(...)を使用することもできますが、具体的な例外型を指定する方が推奨されます。

この記事でわかること
  • 例外とは何か、そして例外処理の必要性
  • try-catchブロックの使い方と、複数の例外をキャッチする方法
  • 例外の再スローの基本と実践例、注意点
  • 標準例外クラスの種類と、カスタム例外クラスの作成方法
  • 応用例として、ネストされたtry-catchブロックや例外を使ったリソース管理

目次から探す

例外処理の基本

例外とは何か

プログラムを実行していると、予期しない事態が発生することがあります。

これを「例外」と呼びます。

例外は、通常のプログラムの流れを中断し、特別な処理を行う必要がある状況を指します。

例えば、ファイルが見つからない、ゼロでの除算、メモリ不足などが例外の例です。

C++では、例外を使ってこれらの問題を検出し、適切に処理することができます。

例外処理の必要性

例外処理は、プログラムの信頼性と安定性を向上させるために重要です。

以下の理由から、例外処理は必要とされます。

  • エラーの検出と報告: 例外を使うことで、エラーが発生した場所と原因を明確にすることができます。
  • プログラムの継続: 例外処理を行うことで、エラーが発生してもプログラムを安全に継続することが可能です。
  • コードの可読性向上: 例外処理を用いることで、エラーチェックのためのコードが整理され、可読性が向上します。

C++における例外処理の基本構文

C++では、例外処理を行うためにtrycatchthrowの3つのキーワードを使用します。

以下に基本的な構文を示します。

#include <iostream>
#include <stdexcept> // 標準例外クラスを使用するために必要
int main() {
    try {
        // 例外を発生させるコード
        throw std::runtime_error("エラーが発生しました");
    } catch (const std::exception& e) {
        // 例外をキャッチして処理するコード
        std::cout << "例外がキャッチされました: " << e.what() << std::endl;
    }
    return 0;
}

このコードでは、tryブロック内で例外が発生すると、catchブロックが実行されます。

throwキーワードを使って例外を発生させ、catchブロックでその例外を受け取って処理します。

std::exceptionは標準ライブラリで提供される例外クラスで、what()メソッドを使ってエラーメッセージを取得できます。

例外がキャッチされました: エラーが発生しました

この例では、std::runtime_errorを使って例外を発生させ、catchブロックでその例外をキャッチしてメッセージを表示しています。

これにより、プログラムはエラーが発生しても安全に終了します。

try-catchブロックの使い方

tryブロックの役割

tryブロックは、例外が発生する可能性のあるコードを囲むために使用されます。

このブロック内で例外が発生すると、プログラムの制御は対応するcatchブロックに移ります。

tryブロックは、例外が発生した場合に備えて、通常のプログラムの流れを中断し、例外処理を行うための準備をします。

#include <iostream>
void riskyFunction() {
    // 例外を発生させる可能性のあるコード
    throw "例外が発生しました"; // 文字列リテラルを投げる
}
int main() {
    try {
        riskyFunction(); // 例外が発生する可能性のある関数を呼び出す
    } catch (...) {
        // 例外をキャッチして処理する
        std::cout << "例外がキャッチされました" << std::endl;
    }
    return 0;
}

catchブロックの役割

catchブロックは、tryブロック内で発生した例外を受け取り、適切に処理するために使用されます。

catchブロックは、例外の型に基づいて特定の例外をキャッチすることができます。

catchブロックは、例外が発生した場合に実行されるコードを含みます。

#include <iostream>
#include <stdexcept>
int main() {
    try {
        throw std::runtime_error("ランタイムエラーが発生しました");
    } catch (const std::runtime_error& e) {
        // std::runtime_error型の例外をキャッチして処理する
        std::cout << "ランタイムエラー: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        // その他のstd::exception型の例外をキャッチして処理する
        std::cout << "例外: " << e.what() << std::endl;
    }
    return 0;
}

複数のcatchブロックの使用方法

C++では、tryブロックに対して複数のcatchブロックを定義することができます。

これにより、異なる型の例外を個別に処理することが可能です。

catchブロックは、特定の例外型に対応しており、例外の型に基づいて適切な処理を行います。

#include <iostream>
#include <stdexcept>
int main() {
    try {
        throw std::out_of_range("範囲外のエラーが発生しました");
    } catch (const std::out_of_range& e) {
        // std::out_of_range型の例外をキャッチして処理する
        std::cout << "範囲外エラー: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        // その他のstd::exception型の例外をキャッチして処理する
        std::cout << "例外: " << e.what() << std::endl;
    } catch (...) {
        // すべての例外をキャッチする
        std::cout << "未知の例外がキャッチされました" << std::endl;
    }
    return 0;
}
範囲外エラー: 範囲外のエラーが発生しました

この例では、std::out_of_range型の例外を最初のcatchブロックでキャッチし、適切なメッセージを表示しています。

その他のstd::exception型の例外は次のcatchブロックで処理され、最後のcatchブロックはすべての例外をキャッチします。

これにより、異なる例外に対して異なる処理を行うことができます。

複数の例外を処理する方法

複数の例外をキャッチする基本構文

C++では、tryブロックに対して複数のcatchブロックを定義することで、異なる例外を個別にキャッチして処理することができます。

catchブロックは、特定の例外型に対応しており、例外の型に基づいて適切な処理を行います。

以下に基本的な構文を示します。

#include <iostream>
#include <stdexcept>
int main() {
    try {
        // 例外を発生させるコード
        throw std::invalid_argument("無効な引数が渡されました");
    } catch (const std::invalid_argument& e) {
        // std::invalid_argument型の例外をキャッチして処理する
        std::cout << "無効な引数エラー: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        // その他のstd::exception型の例外をキャッチして処理する
        std::cout << "例外: " << e.what() << std::endl;
    }
    return 0;
}

異なる型の例外をキャッチする

異なる型の例外をキャッチするためには、catchブロックをそれぞれの例外型に対して用意します。

これにより、特定の例外に対して異なる処理を行うことが可能です。

以下の例では、std::invalid_argumentstd::out_of_rangeの2つの異なる例外をキャッチしています。

#include <iostream>
#include <stdexcept>
int main() {
    try {
        // 例外を発生させるコード
        throw std::out_of_range("範囲外のエラーが発生しました");
    } catch (const std::invalid_argument& e) {
        // std::invalid_argument型の例外をキャッチして処理する
        std::cout << "無効な引数エラー: " << e.what() << std::endl;
    } catch (const std::out_of_range& e) {
        // std::out_of_range型の例外をキャッチして処理する
        std::cout << "範囲外エラー: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        // その他のstd::exception型の例外をキャッチして処理する
        std::cout << "例外: " << e.what() << std::endl;
    }
    return 0;
}

共通の基底クラスを使った例外処理

C++の標準例外クラスは、std::exceptionを基底クラスとして継承されています。

これにより、共通の基底クラスを使って例外をキャッチすることができます。

std::exceptionを使うことで、すべての標準例外を一括して処理することが可能です。

#include <iostream>
#include <stdexcept>
int main() {
    try {
        // 例外を発生させるコード
        throw std::runtime_error("ランタイムエラーが発生しました");
    } catch (const std::exception& e) {
        // std::exception型の例外をキャッチして処理する
        std::cout << "例外: " << e.what() << std::endl;
    }
    return 0;
}
例外: ランタイムエラーが発生しました

この例では、std::runtime_error型の例外をstd::exception型catchブロックでキャッチしています。

std::exceptionを基底クラスとして使用することで、すべての標準例外を一括して処理することができ、コードの簡潔さと保守性が向上します。

例外の再スロー

再スローの基本

例外の再スローとは、catchブロック内でキャッチした例外を再度スローすることを指します。

再スローを行うことで、例外をさらに上位の呼び出し元に伝播させることができます。

これにより、例外の詳細な処理を呼び出し元で行うことが可能になります。

再スローは、catchブロック内でthrow;と記述することで行います。

#include <iostream>
#include <stdexcept>
void functionThatThrows() {
    // 例外を発生させる
    throw std::runtime_error("エラーが発生しました");
}
int main() {
    try {
        functionThatThrows(); // 例外を発生させる関数を呼び出す
    } catch (const std::exception& e) {
        // 例外をキャッチして再スローする
        std::cout << "例外をキャッチしましたが、再スローします: " << e.what() << std::endl;
        throw; // 再スロー
    }
    return 0;
}

再スローの実践例

再スローは、例外のログを記録したり、特定の処理を行った後に例外を上位に伝播させる場合に有用です。

以下の例では、例外をキャッチしてログを記録した後、再スローしています。

#include <iostream>
#include <stdexcept>
void logAndRethrow() {
    try {
        throw std::invalid_argument("無効な引数が渡されました");
    } catch (const std::invalid_argument& e) {
        // 例外のログを記録
        std::cout << "ログ: " << e.what() << std::endl;
        throw; // 再スロー
    }
}
int main() {
    try {
        logAndRethrow(); // 例外を発生させる関数を呼び出す
    } catch (const std::exception& e) {
        // 上位で例外をキャッチして処理する
        std::cout << "上位で例外をキャッチ: " << e.what() << std::endl;
    }
    return 0;
}
ログ: 無効な引数が渡されました
上位で例外をキャッチ: 無効な引数が渡されました

この例では、logAndRethrow関数内で例外をキャッチし、ログを記録した後に再スローしています。

main関数で再度例外をキャッチし、最終的な処理を行っています。

再スローの注意点

再スローを行う際には、以下の点に注意が必要です。

  • 例外の情報を失わない: 再スローする際に、例外の情報を失わないように注意します。

throw;を使うことで、元の例外情報を保持したまま再スローできます。

  • 例外の伝播を意識する: 再スローによって例外がさらに上位に伝播するため、呼び出し元で適切に例外を処理する必要があります。
  • パフォーマンスへの影響: 再スローは例外処理のオーバーヘッドを増加させる可能性があるため、必要な場合にのみ使用します。

再スローは、例外処理の柔軟性を高めるための強力な手段ですが、適切に使用することが重要です。

標準例外クラスの活用

標準例外クラスの種類

C++の標準ライブラリには、さまざまな例外クラスが用意されています。

これらのクラスは、一般的なエラー状況を表現するために使用されます。

以下に、主な標準例外クラスを示します。

スクロールできます
クラス名説明
std::exceptionすべての標準例外の基底クラス
std::runtime_error実行時エラーを表す例外
std::logic_error論理エラーを表す例外
std::invalid_argument無効な引数が渡された場合の例外
std::out_of_range範囲外アクセスが行われた場合の例外
std::overflow_error算術オーバーフローが発生した場合の例外
std::underflow_error算術アンダーフローが発生した場合の例外

これらのクラスを使用することで、特定のエラー状況に応じた例外処理を行うことができます。

std::exceptionの使い方

std::exceptionは、すべての標準例外クラスの基底クラスです。

このクラスは、例外の基本的な機能を提供し、what()メソッドを使ってエラーメッセージを取得することができます。

以下にstd::exceptionの基本的な使い方を示します。

#include <iostream>
#include <stdexcept>
int main() {
    try {
        // 例外を発生させる
        throw std::runtime_error("ランタイムエラーが発生しました");
    } catch (const std::exception& e) {
        // std::exception型の例外をキャッチして処理する
        std::cout << "例外: " << e.what() << std::endl;
    }
    return 0;
}
例外: ランタイムエラーが発生しました

この例では、std::runtime_error型の例外をstd::exception型catchブロックでキャッチし、エラーメッセージを表示しています。

カスタム例外クラスの作成

標準例外クラスでは対応できない特定のエラー状況に対しては、カスタム例外クラスを作成することができます。

カスタム例外クラスは、std::exceptionを継承して作成します。

以下にカスタム例外クラスの例を示します。

#include <iostream>
#include <exception>
#include <string>
// カスタム例外クラスの定義
class CustomException : public std::exception {
private:
    std::string message;
public:
    explicit CustomException(const std::string& msg) : message(msg) {}
    const char* what() const noexcept override {
        return message.c_str();
    }
};
int main() {
    try {
        // カスタム例外を発生させる
        throw CustomException("カスタム例外が発生しました");
    } catch (const CustomException& e) {
        // カスタム例外をキャッチして処理する
        std::cout << "カスタム例外: " << e.what() << std::endl;
    }
    return 0;
}
カスタム例外: カスタム例外が発生しました

この例では、CustomExceptionというカスタム例外クラスを定義し、std::exceptionを継承しています。

what()メソッドをオーバーライドして、カスタムメッセージを返すようにしています。

これにより、特定のエラー状況に応じた例外処理を柔軟に行うことができます。

応用例

ネストされたtry-catchブロック

ネストされたtry-catchブロックを使用することで、異なるレベルでの例外処理を行うことができます。

これにより、特定の処理に対して局所的な例外処理を行い、さらに上位で一般的な例外処理を行うことが可能です。

#include <iostream>
#include <stdexcept>
int main() {
    try {
        try {
            // 内部の例外を発生させる
            throw std::invalid_argument("無効な引数が渡されました");
        } catch (const std::invalid_argument& e) {
            // 内部の例外をキャッチして処理する
            std::cout << "内部キャッチ: " << e.what() << std::endl;
            throw; // 再スロー
        }
    } catch (const std::exception& e) {
        // 外部の例外をキャッチして処理する
        std::cout << "外部キャッチ: " << e.what() << std::endl;
    }
    return 0;
}
内部キャッチ: 無効な引数が渡されました
外部キャッチ: 無効な引数が渡されました

この例では、内部のtry-catchブロックで例外をキャッチして再スローし、外部のtry-catchブロックで最終的な処理を行っています。

例外のチェーン処理

例外のチェーン処理を行うことで、異なる例外を連鎖的に処理することができます。

これにより、例外の原因を詳細に追跡することが可能です。

#include <iostream>
#include <stdexcept>
void functionA() {
    try {
        throw std::runtime_error("関数Aでエラーが発生しました");
    } catch (const std::runtime_error& e) {
        // 例外をキャッチして新しい例外をスロー
        throw std::logic_error("関数Aのエラーを処理中に新しいエラーが発生しました");
    }
}
int main() {
    try {
        functionA();
    } catch (const std::exception& e) {
        // 最終的な例外をキャッチして処理する
        std::cout << "最終キャッチ: " << e.what() << std::endl;
    }
    return 0;
}
最終キャッチ: 関数Aのエラーを処理中に新しいエラーが発生しました

この例では、functionA内で例外をキャッチし、新しい例外をスローしています。

これにより、例外の原因を追跡しつつ、新たなエラー状況を報告しています。

例外を使ったリソース管理

例外を使ったリソース管理は、リソースの確保と解放を安全に行うための手法です。

C++では、RAII(Resource Acquisition Is Initialization)というパターンを用いて、例外が発生してもリソースが適切に解放されるようにします。

#include <iostream>
#include <stdexcept>
class Resource {
public:
    Resource() { std::cout << "リソースを確保しました" << std::endl; }
    ~Resource() { std::cout << "リソースを解放しました" << std::endl; }
};
int main() {
    try {
        Resource res; // リソースの確保
        throw std::runtime_error("エラーが発生しました");
    } catch (const std::exception& e) {
        std::cout << "例外: " << e.what() << std::endl;
    }
    return 0;
}
リソースを確保しました
リソースを解放しました
例外: エラーが発生しました

この例では、Resourceクラスのデストラクタが例外発生時にも呼び出され、リソースが適切に解放されます。

例外安全なコードの書き方

例外安全なコードを書くためには、例外が発生してもプログラムの状態が一貫性を保つようにする必要があります。

以下のポイントに注意してコードを書くと、例外安全性が向上します。

  • RAIIの利用: リソースの確保と解放をコンストラクタとデストラクタで行う。
  • 例外をスローしないデストラクタ: デストラクタ内で例外をスローしないようにする。
  • 強い例外保証: 例外が発生しても、オブジェクトの状態が変更されないようにする。
  • 例外の伝播を考慮: 例外が発生した場合の処理を明確にし、必要に応じて再スローを行う。

これらの手法を用いることで、例外が発生してもプログラムが安全に動作し続けることができます。

よくある質問

複数の例外をキャッチする際のパフォーマンスはどうなる?

複数の例外をキャッチする際のパフォーマンスは、通常のプログラムの実行に比べてオーバーヘッドが発生します。

例外処理は、スタックの巻き戻しや例外オブジェクトの生成など、通常の制御フローとは異なる処理を行うため、パフォーマンスに影響を与えることがあります。

しかし、例外は通常、異常な状況でのみ発生するため、通常のプログラムの実行パスではパフォーマンスへの影響は限定的です。

したがって、例外処理は、エラーが発生した場合のみに備えるものであり、通常の制御フローで頻繁に使用するべきではありません。

例外処理を使わない方が良い場合はある?

例外処理を使わない方が良い場合もあります。

以下のような状況では、例外処理を避けることが推奨されます。

  • パフォーマンスが重要な場合: 例外処理はオーバーヘッドがあるため、リアルタイムシステムやパフォーマンスが非常に重要なシステムでは、例外処理を避けることがあります。
  • 予測可能なエラー: 予測可能なエラー(例:ユーザー入力の検証)に対しては、通常のエラーチェックを行う方が適切です。
  • リソース制約がある場合: メモリやCPUリソースが限られている環境では、例外処理のオーバーヘッドが問題になることがあります。

例外処理とエラーハンドリングの違いは何ですか?

例外処理とエラーハンドリングは、エラーを管理するための異なるアプローチです。

  • 例外処理: プログラムの通常の制御フローを中断し、特別な処理を行うためのメカニズムです。

例外は、予期しないエラーや異常な状況を処理するために使用されます。

例外処理は、trycatchthrowキーワードを使用して実装されます。

  • エラーハンドリング: 通常の制御フロー内でエラーを検出し、適切に処理する方法です。

エラーハンドリングは、関数の戻り値やエラーフラグを使用してエラーを管理します。

予測可能なエラーや軽微なエラーに対しては、エラーハンドリングが適しています。

例外処理は、異常な状況に対する強力なメカニズムですが、すべてのエラーに対して使用する必要はありません。

エラーハンドリングと例外処理を適切に使い分けることが重要です。

まとめ

この記事では、C++における例外処理の基本から応用までを詳しく解説し、複数の例外を効率的に処理する方法や、例外を活用したリソース管理のテクニックについても触れました。

例外処理は、プログラムの信頼性と安定性を高めるための重要な手法であり、適切に使用することで、エラー発生時にも安全にプログラムを継続させることが可能です。

この記事を参考に、実際のプログラムで例外処理を活用し、より堅牢なコードを書くことに挑戦してみてください。

当サイトはリンクフリーです。出典元を明記していただければ、ご自由に引用していただいて構いません。

関連カテゴリーから探す

  • URLをコピーしました!
目次から探す