C++のtry, catch, throwの使い方を徹底解説

C++では、プログラムの実行中に発生するエラーを効果的に管理するために例外処理が重要な役割を果たします。本記事では、C++の例外処理の基本となるtry, catch, throwキーワードの使い方を詳しく説明し、実際のコード例を通じてその概念を深く理解します。

目次
  1. 例外処理の基礎
    1. 例外処理の重要性
    2. 例外処理の基本構造
  2. tryブロックの使い方
    1. tryブロックの基本構造
    2. 具体的な例
    3. 複数のtryブロック
  3. catchブロックの使い方
    1. catchブロックの基本構造
    2. 具体的な例
    3. 複数のcatchブロック
    4. catchブロックでの例外処理
  4. throwキーワードの使い方
    1. throwキーワードの基本構造
    2. 具体的な例
    3. 例外オブジェクトの種類
    4. カスタム例外の作成
    5. 例外の再スロー
  5. ネストされたtry-catchブロック
    1. 基本的なネストされたtry-catchブロックの構造
    2. 具体的な例
    3. 複数レベルの例外処理
  6. カスタム例外クラスの作成
    1. カスタム例外クラスの基本構造
    2. カスタム例外クラスの使用例
    3. カスタム例外クラスの拡張
  7. 例外の再スロー
    1. 基本的な再スローの構造
    2. 具体的な例
    3. 再スローの用途
    4. 再スローの注意点
  8. 標準ライブラリの例外クラス
    1. 標準例外クラスの概要
    2. 具体的な使用例
    3. 標準例外クラスの階層構造
    4. 標準例外クラスの使用の利点
  9. 例外処理のベストプラクティス
    1. 適切な例外の使用
    2. 例外の種類を使い分ける
    3. 例外メッセージの明確化
    4. リソースの管理
    5. catchブロックでの具体的な例外の捕捉
    6. 例外の再スローを慎重に行う
    7. 無駄なcatchブロックを避ける
  10. 演習問題と応用例
    1. 演習問題 1: 範囲外アクセスの例外処理
    2. 演習問題 2: カスタム例外クラスの作成
    3. 演習問題 3: 再スローの実装
    4. 応用例: ファイル読み込みと例外処理
  11. まとめ

例外処理の基礎

C++における例外処理は、プログラムの実行中に発生するエラーを捕捉し、適切に対処するためのメカニズムです。例外処理を使用することで、エラーが発生した際にプログラムがクラッシュするのを防ぎ、ユーザーに適切なフィードバックを提供できます。

例外処理の重要性

例外処理は、次の理由で重要です。

  1. 安定性の向上: プログラムが予期せぬエラーでクラッシュするのを防ぎます。
  2. デバッグの容易さ: エラーの原因を特定しやすくなります。
  3. ユーザー体験の向上: エラー発生時に適切なメッセージを表示し、ユーザーが混乱しないようにします。

例外処理の基本構造

例外処理は、主に次の3つのキーワードで構成されます。

  1. try: 例外が発生する可能性のあるコードブロックを囲みます。
  2. catch: 発生した例外を捕捉し、適切な処理を行います。
  3. throw: 例外を発生させます。

以下は、基本的な例外処理の構造を示すコード例です。

#include <iostream>

int main() {
    try {
        // 例外が発生する可能性のあるコード
        throw std::runtime_error("エラーが発生しました");
    } catch (const std::exception& e) {
        // 例外を捕捉し、処理を行うコード
        std::cerr << "例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、throwキーワードを使って例外を発生させ、catchブロックでその例外を捕捉し、エラーメッセージを表示しています。

tryブロックの使い方

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

tryブロックの基本構造

tryブロックの基本的な構造は次のようになります。

try {
    // 例外が発生する可能性のあるコード
} catch (例外の型& 例外の変数) {
    // 例外を捕捉し、処理を行うコード
}

具体的な例

以下の例は、ゼロでの除算を試みるコードをtryブロックで囲み、catchブロックで例外を捕捉する方法を示しています。

#include <iostream>

int main() {
    int a = 10;
    int b = 0;
    try {
        if (b == 0) {
            throw std::runtime_error("ゼロでの除算は許可されていません");
        }
        int c = a / b;
    } catch (const std::exception& e) {
        std::cerr << "例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、bがゼロの場合にthrowキーワードを使って例外を発生させています。catchブロックでこの例外を捕捉し、エラーメッセージを表示します。

複数のtryブロック

プログラム内で複数のtryブロックを使用することもできます。各tryブロックには対応するcatchブロックが必要です。

#include <iostream>

int main() {
    try {
        // 最初のtryブロック
        throw std::runtime_error("最初の例外");
    } catch (const std::exception& e) {
        std::cerr << "最初の例外が捕捉されました: " << e.what() << std::endl;
    }

    try {
        // 二番目のtryブロック
        throw std::runtime_error("二番目の例外");
    } catch (const std::exception& e) {
        std::cerr << "二番目の例外が捕捉されました: " << e.what() << std::endl;
    }

    return 0;
}

このように、プログラムの異なる部分で発生する可能性のある例外を、それぞれのtryブロックで管理することができます。

catchブロックの使い方

catchブロックは、tryブロック内で発生した例外を捕捉し、適切に処理するために使用されます。catchブロックは、例外の型と変数を受け取ることで、特定の例外を捕捉できます。

catchブロックの基本構造

catchブロックの基本的な構造は次のようになります。

try {
    // 例外が発生する可能性のあるコード
} catch (例外の型& 例外の変数) {
    // 例外を捕捉し、処理を行うコード
}

具体的な例

以下の例は、異なる種類の例外を捕捉するために複数のcatchブロックを使用する方法を示しています。

#include <iostream>
#include <stdexcept>

int main() {
    try {
        throw std::runtime_error("ランタイムエラーが発生しました");
    } catch (const std::runtime_error& e) {
        std::cerr << "ランタイムエラーが捕捉されました: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "その他の例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、最初のcatchブロックでstd::runtime_error型の例外を捕捉し、次のcatchブロックで他の全てのstd::exception型の例外を捕捉しています。

複数のcatchブロック

複数のcatchブロックを使用することで、異なる種類の例外に対して異なる処理を行うことができます。

#include <iostream>
#include <stdexcept>

int main() {
    try {
        throw std::out_of_range("範囲外アクセス");
    } catch (const std::out_of_range& e) {
        std::cerr << "範囲外エラーが捕捉されました: " << e.what() << std::endl;
    } catch (const std::runtime_error& e) {
        std::cerr << "ランタイムエラーが捕捉されました: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "その他の例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、std::out_of_range型、std::runtime_error型、その他のstd::exception型の例外をそれぞれ異なるcatchブロックで捕捉し、対応しています。

catchブロックでの例外処理

catchブロック内では、例外の情報をログに記録したり、適切なエラーメッセージをユーザーに表示したりすることが一般的です。また、リソースの解放やクリーンアップなどの処理もここで行います。

#include <iostream>
#include <fstream>
#include <stdexcept>

int main() {
    std::ifstream file;
    try {
        file.open("example.txt");
        if (!file.is_open()) {
            throw std::runtime_error("ファイルを開けませんでした");
        }
        // ファイル操作
    } catch (const std::exception& e) {
        std::cerr << "例外が捕捉されました: " << e.what() << std::endl;
    } finally {
        if (file.is_open()) {
            file.close();
        }
    }
    return 0;
}

この例では、ファイルを開く操作が失敗した場合に例外を発生させ、catchブロックでその例外を捕捉してエラーメッセージを表示します。ファイルが開いている場合にはfinallyブロックでファイルを閉じています。

throwキーワードの使い方

throwキーワードは、プログラム内で例外を明示的に発生させるために使用されます。throwキーワードを使うことで、異常な状況やエラーが発生したことを呼び出し元に通知し、適切な処理を行わせることができます。

throwキーワードの基本構造

throwキーワードは、以下のように使用します。

throw 例外オブジェクト;

具体的な例

以下の例は、ゼロでの除算を試みた際に例外を発生させる方法を示しています。

#include <iostream>
#include <stdexcept>

int divide(int a, int b) {
    if (b == 0) {
        throw std::invalid_argument("ゼロでの除算は許可されていません");
    }
    return a / b;
}

int main() {
    try {
        int result = divide(10, 0);
    } catch (const std::invalid_argument& e) {
        std::cerr << "例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、divide関数内でゼロでの除算を試みた際にstd::invalid_argument型の例外をthrowキーワードで発生させ、main関数内のcatchブロックでその例外を捕捉しています。

例外オブジェクトの種類

C++標準ライブラリには、さまざまな例外オブジェクトが定義されています。代表的な例外オブジェクトには以下のものがあります。

  • std::exception: すべての標準例外の基本クラス
  • std::runtime_error: 実行時エラーを示す例外
  • std::invalid_argument: 無効な引数を示す例外
  • std::out_of_range: 範囲外アクセスを示す例外

カスタム例外の作成

独自の例外クラスを作成することもできます。以下は、カスタム例外クラスを定義し、それをthrowする例です。

#include <iostream>
#include <stdexcept>

class MyCustomException : public std::exception {
public:
    const char* what() const noexcept override {
        return "カスタム例外が発生しました";
    }
};

void testFunction() {
    throw MyCustomException();
}

int main() {
    try {
        testFunction();
    } catch (const MyCustomException& e) {
        std::cerr << "例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、MyCustomExceptionというカスタム例外クラスを定義し、testFunction関数内でその例外をthrowしています。main関数内でその例外を捕捉し、メッセージを表示します。

例外の再スロー

捕捉した例外を再度スローすることもできます。これにより、例外を呼び出し元に伝搬させることができます。

#include <iostream>
#include <stdexcept>

void functionA() {
    try {
        throw std::runtime_error("functionAでエラーが発生");
    } catch (...) {
        std::cerr << "functionAで例外を捕捉しましたが再スローします" << std::endl;
        throw;  // 例外を再スロー
    }
}

int main() {
    try {
        functionA();
    } catch (const std::exception& e) {
        std::cerr << "main関数で例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、functionAで発生した例外を捕捉し、再度スローしてmain関数で捕捉しています。

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

ネストされたtry-catchブロックを使用することで、複雑な例外処理を行うことができます。これは、あるtryブロック内にさらに別のtry-catchブロックを配置する方法です。これにより、異なるレベルのエラーに対して個別に対応することが可能になります。

基本的なネストされたtry-catchブロックの構造

ネストされたtry-catchブロックの基本構造は次のようになります。

try {
    // 外側のtryブロック
    try {
        // 内側のtryブロック
    } catch (例外の型& 例外の変数) {
        // 内側のcatchブロック
    }
} catch (例外の型& 例外の変数) {
    // 外側のcatchブロック
}

具体的な例

以下の例は、ファイル操作と数値の計算を行う際のネストされたtry-catchブロックを示しています。

#include <iostream>
#include <fstream>
#include <stdexcept>

void readFileAndCalculate(const std::string& filename) {
    std::ifstream file;
    try {
        file.open(filename);
        if (!file.is_open()) {
            throw std::runtime_error("ファイルを開けませんでした");
        }

        try {
            int a = 10;
            int b = 0;
            if (b == 0) {
                throw std::invalid_argument("ゼロでの除算は許可されていません");
            }
            int c = a / b;
        } catch (const std::invalid_argument& e) {
            std::cerr << "計算エラーが発生しました: " << e.what() << std::endl;
        }

    } catch (const std::runtime_error& e) {
        std::cerr << "ファイルエラーが発生しました: " << e.what() << std::endl;
    } finally {
        if (file.is_open()) {
            file.close();
        }
    }
}

int main() {
    try {
        readFileAndCalculate("example.txt");
    } catch (const std::exception& e) {
        std::cerr << "メイン関数で例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、readFileAndCalculate関数内でファイルの読み込みを行い、その中でさらに計算を行うtryブロックをネストさせています。ファイルが開けない場合は外側のcatchブロックで処理され、計算中にエラーが発生した場合は内側のcatchブロックで処理されます。

複数レベルの例外処理

ネストされたtry-catchブロックは、複数レベルの例外処理を行う際に非常に有効です。たとえば、以下のようなケースがあります。

#include <iostream>
#include <stdexcept>

void levelThreeFunction() {
    throw std::runtime_error("レベル3でエラーが発生");
}

void levelTwoFunction() {
    try {
        levelThreeFunction();
    } catch (const std::runtime_error& e) {
        std::cerr << "レベル2で例外を捕捉しました: " << e.what() << std::endl;
        throw; // 例外を再スロー
    }
}

void levelOneFunction() {
    try {
        levelTwoFunction();
    } catch (const std::runtime_error& e) {
        std::cerr << "レベル1で例外を捕捉しました: " << e.what() << std::endl;
    }
}

int main() {
    try {
        levelOneFunction();
    } catch (const std::exception& e) {
        std::cerr << "メイン関数で例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、levelThreeFunctionで発生した例外がlevelTwoFunctionで捕捉され、再スローされてlevelOneFunctionで再度捕捉されます。最終的にメイン関数で例外が捕捉されます。このように、例外の伝搬と処理を複数レベルで行うことができます。

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

C++では、独自の例外クラスを作成することで、特定のエラー状況に対してより詳細な情報を提供することができます。カスタム例外クラスを作成することで、エラーの種類や原因を明確にし、より適切なエラーハンドリングを実現できます。

カスタム例外クラスの基本構造

カスタム例外クラスを作成するには、std::exceptionクラスを継承し、必要なメンバー関数をオーバーライドします。以下は、基本的なカスタム例外クラスの構造です。

#include <iostream>
#include <exception>

class MyCustomException : public std::exception {
public:
    MyCustomException(const std::string& message) : message_(message) {}
    const char* what() const noexcept override {
        return message_.c_str();
    }
private:
    std::string message_;
};

カスタム例外クラスの使用例

カスタム例外クラスを使用する具体例を示します。ここでは、特定の条件下でカスタム例外を発生させ、それを捕捉して処理します。

#include <iostream>
#include <exception>

class MyCustomException : public std::exception {
public:
    MyCustomException(const std::string& message) : message_(message) {}
    const char* what() const noexcept override {
        return message_.c_str();
    }
private:
    std::string message_;
};

void checkValue(int value) {
    if (value < 0) {
        throw MyCustomException("負の値は許可されていません: " + std::to_string(value));
    }
}

int main() {
    try {
        checkValue(-10);
    } catch (const MyCustomException& e) {
        std::cerr << "カスタム例外が捕捉されました: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "標準例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、checkValue関数内で負の値をチェックし、負の値が見つかった場合にMyCustomExceptionをスローします。main関数では、このカスタム例外を捕捉し、エラーメッセージを表示します。

カスタム例外クラスの拡張

カスタム例外クラスにさらに情報を追加することで、エラーの詳細をより多く提供することができます。以下は、エラーコードを持つカスタム例外クラスの例です。

#include <iostream>
#include <exception>

class ExtendedException : public std::exception {
public:
    ExtendedException(const std::string& message, int errorCode)
        : message_(message), errorCode_(errorCode) {}
    const char* what() const noexcept override {
        return message_.c_str();
    }
    int getErrorCode() const noexcept {
        return errorCode_;
    }
private:
    std::string message_;
    int errorCode_;
};

void performOperation(int value) {
    if (value == 0) {
        throw ExtendedException("ゼロは無効な値です", 1001);
    }
}

int main() {
    try {
        performOperation(0);
    } catch (const ExtendedException& e) {
        std::cerr << "カスタム例外が捕捉されました: " << e.what() << std::endl;
        std::cerr << "エラーコード: " << e.getErrorCode() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "標準例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、ExtendedExceptionクラスがエラーコードを持つように拡張されています。performOperation関数内で無効な値が見つかった場合にこのカスタム例外をスローし、main関数でそのエラーコードを含むメッセージを表示します。

カスタム例外クラスを作成することで、より柔軟で詳細なエラー処理が可能になります。これにより、プログラムの信頼性と保守性が向上します。

例外の再スロー

例外の再スローは、捕捉した例外を再度スローして呼び出し元に伝搬させるために使用されます。これにより、例外を捕捉して一部の処理を行った後、さらに上位のレベルで例外を処理させることができます。

基本的な再スローの構造

再スローの基本構造は次のようになります。catchブロック内で再スローを行うには、throwキーワードを使用します。

try {
    // 例外が発生する可能性のあるコード
} catch (例外の型& 例外の変数) {
    // 例外を捕捉し、一部の処理を行う
    throw; // 例外を再スロー
}

具体的な例

以下の例は、再スローを使用して例外を上位の呼び出し元に伝搬させる方法を示しています。

#include <iostream>
#include <stdexcept>

void functionA() {
    try {
        throw std::runtime_error("functionAでエラーが発生");
    } catch (const std::runtime_error& e) {
        std::cerr << "functionAで例外を捕捉しましたが再スローします: " << e.what() << std::endl;
        throw; // 例外を再スロー
    }
}

void functionB() {
    try {
        functionA();
    } catch (const std::runtime_error& e) {
        std::cerr << "functionBで例外を捕捉しました: " << e.what() << std::endl;
    }
}

int main() {
    try {
        functionB();
    } catch (const std::exception& e) {
        std::cerr << "main関数で例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、functionAで発生した例外を捕捉し、一部の処理を行った後で再スローしています。functionBで再度その例外を捕捉し、最終的にmain関数でも捕捉されます。

再スローの用途

再スローは、特定の状況で非常に有用です。以下のような用途があります。

  • ロギング: 例外を捕捉してエラーメッセージをログに記録し、その後例外を再スローする。
  • リソースのクリーンアップ: 一時的なリソースのクリーンアップを行い、その後例外を再スローする。
  • 中間処理: 例外の情報を一部加工または追加して再スローする。

再スローの注意点

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

  • 例外の情報を損なわない: 再スローする際に、元の例外の情報を失わないようにする。
  • 適切なレベルで処理する: 再スローした例外が適切なレベルで最終的に処理されるように設計する。
  • 無限ループを避ける: 再スローが無限ループに陥らないように注意する。

以下は、再スローを行う際の良い例です。

#include <iostream>
#include <stdexcept>

class CustomException : public std::exception {
public:
    CustomException(const std::string& message) : message_(message) {}
    const char* what() const noexcept override {
        return message_.c_str();
    }
private:
    std::string message_;
};

void riskyOperation() {
    throw CustomException("重大なエラーが発生しました");
}

void handleOperation() {
    try {
        riskyOperation();
    } catch (const CustomException& e) {
        std::cerr << "エラーをログに記録しました: " << e.what() << std::endl;
        throw; // 再スロー
    }
}

int main() {
    try {
        handleOperation();
    } catch (const CustomException& e) {
        std::cerr << "main関数でカスタム例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、riskyOperationで発生したカスタム例外をhandleOperationで一度捕捉し、ログに記録した後で再スローしています。最終的にmain関数で例外を捕捉して処理します。

標準ライブラリの例外クラス

C++標準ライブラリには、さまざまな例外クラスが定義されており、これらを利用することで一般的なエラー状況を簡単に処理することができます。標準ライブラリの例外クラスは、エラーメッセージの表示やエラー処理の一貫性を保つために非常に有用です。

標準例外クラスの概要

C++標準ライブラリには、いくつかの基本的な例外クラスが含まれています。これらのクラスは、std::exceptionクラスを基底クラスとし、特定のエラー状況を示すために派生クラスとして定義されています。以下は、主要な標準例外クラスの一覧です。

  • std::exception: すべての標準例外の基本クラス
  • std::runtime_error: 実行時エラーを示す例外
  • std::logic_error: 論理エラーを示す例外
  • std::invalid_argument: 無効な引数を示す例外
  • std::out_of_range: 範囲外アクセスを示す例外
  • std::length_error: 長さエラーを示す例外
  • std::bad_alloc: メモリ割り当ての失敗を示す例外

具体的な使用例

以下は、標準ライブラリの例外クラスを使用した具体的な例です。

#include <iostream>
#include <stdexcept>
#include <vector>

int main() {
    try {
        std::vector<int> vec = {1, 2, 3};
        std::cout << vec.at(5) << std::endl; // 範囲外アクセス
    } catch (const std::out_of_range& e) {
        std::cerr << "範囲外エラーが捕捉されました: " << e.what() << std::endl;
    }

    try {
        int a = -1;
        if (a < 0) {
            throw std::invalid_argument("負の数は無効です");
        }
    } catch (const std::invalid_argument& e) {
        std::cerr << "無効な引数エラーが捕捉されました: " << e.what() << std::endl;
    }

    try {
        int* largeArray = new int[100000000000000];
    } catch (const std::bad_alloc& e) {
        std::cerr << "メモリ割り当てエラーが捕捉されました: " << e.what() << std::endl;
    }

    return 0;
}

この例では、以下の標準例外クラスを使用しています。

  • std::out_of_range: ベクターの範囲外アクセスを試みた場合にスローされる例外を捕捉。
  • std::invalid_argument: 負の数を無効な引数としてスローし、捕捉。
  • std::bad_alloc: メモリ割り当ての失敗時にスローされる例外を捕捉。

標準例外クラスの階層構造

標準例外クラスは階層構造を持っています。std::exceptionが基底クラスであり、他のすべての標準例外クラスはこれを継承しています。

std::exception
├── std::logic_error
│   ├── std::invalid_argument
│   ├── std::length_error
│   └── std::out_of_range
└── std::runtime_error
    ├── std::overflow_error
    ├── std::underflow_error
    └── std::range_error

この階層構造により、特定の例外だけでなく、より一般的な例外クラスも捕捉することができます。

標準例外クラスの使用の利点

  • 再利用性: 標準ライブラリの例外クラスは再利用可能であり、コードの一貫性を保つのに役立ちます。
  • メンテナンス性: 一貫した例外処理により、コードのメンテナンスが容易になります。
  • 情報の提供: 標準例外クラスは、エラーの詳細を提供するためのwhatメソッドを持っており、エラーメッセージを取得できます。

標準ライブラリの例外クラスを活用することで、エラー処理を効率的かつ効果的に行うことができます。これにより、プログラムの信頼性とユーザー体験が向上します。

例外処理のベストプラクティス

例外処理を効果的に行うためには、いくつかのベストプラクティスを遵守することが重要です。これにより、コードの可読性とメンテナンス性が向上し、エラー発生時のトラブルシューティングが容易になります。

適切な例外の使用

例外は、本当に異常な状況でのみ使用すべきです。予測可能なエラーや通常の制御フローのために例外を使用するのは避けるべきです。

try {
    int index = 10;
    if (index < 0 || index >= vec.size()) {
        throw std::out_of_range("インデックスが範囲外です");
    }
    int value = vec.at(index);
} catch (const std::out_of_range& e) {
    std::cerr << "エラー: " << e.what() << std::endl;
}

例外の種類を使い分ける

適切な例外クラスを選択し、エラーの種類に応じた例外をスローすることが重要です。これにより、例外の捕捉と処理が容易になります。

try {
    // ファイルを開く
    if (!file.is_open()) {
        throw std::runtime_error("ファイルを開けませんでした");
    }
} catch (const std::runtime_error& e) {
    std::cerr << "ランタイムエラーが発生: " << e.what() << std::endl;
}

例外メッセージの明確化

例外をスローする際には、明確で具体的なメッセージを提供することが重要です。これにより、エラーの原因を迅速に特定できます。

throw std::invalid_argument("無効な引数: 値が負の数です");

リソースの管理

例外が発生してもリソースが正しく解放されるようにするためには、RAII(Resource Acquisition Is Initialization)パターンを使用することが推奨されます。

#include <memory>

void example() {
    std::unique_ptr<int[]> data(new int[100]);
    // データの操作
    if (error_condition) {
        throw std::runtime_error("エラーが発生しました");
    }
    // データの使用
}

catchブロックでの具体的な例外の捕捉

具体的な例外クラスを先に捕捉し、一般的な例外クラスを後に捕捉するようにします。これにより、より詳細なエラーメッセージや処理を行うことができます。

try {
    // 例外が発生する可能性のあるコード
} catch (const std::out_of_range& e) {
    std::cerr << "範囲外エラーが捕捉されました: " << e.what() << std::endl;
} catch (const std::exception& e) {
    std::cerr << "一般的な例外が捕捉されました: " << e.what() << std::endl;
}

例外の再スローを慎重に行う

再スローする場合は、必要な場合に限り行い、例外の情報を適切に保持するようにします。

try {
    // 例外が発生する可能性のあるコード
} catch (const std::exception& e) {
    // ログを記録するなどの処理
    throw; // 例外を再スロー
}

無駄なcatchブロックを避ける

無駄なcatchブロックを避け、適切な場所で例外を処理するようにします。全ての例外を無条件に捕捉して無視することは避けるべきです。

try {
    // 例外が発生する可能性のあるコード
} catch (...) {
    std::cerr << "予期しないエラーが発生しました" << std::endl;
}

これらのベストプラクティスを遵守することで、例外処理が効果的かつ効率的に行われ、プログラムの信頼性と保守性が向上します。

演習問題と応用例

例外処理の理解を深めるために、実際にコードを書いて試してみましょう。ここでは、いくつかの演習問題と応用例を紹介します。

演習問題 1: 範囲外アクセスの例外処理

以下のコードは、配列の範囲外アクセスを試みます。これをtry-catchブロックで囲み、例外を適切に処理してください。

#include <iostream>
#include <vector>
#include <stdexcept>

void accessVectorElement(const std::vector<int>& vec, int index) {
    // TODO: ここでtry-catchブロックを使って範囲外アクセスを処理する
    std::cout << vec.at(index) << std::endl;
}

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    accessVectorElement(vec, 10); // 範囲外アクセス
    return 0;
}

解答例

#include <iostream>
#include <vector>
#include <stdexcept>

void accessVectorElement(const std::vector<int>& vec, int index) {
    try {
        std::cout << vec.at(index) << std::endl;
    } catch (const std::out_of_range& e) {
        std::cerr << "範囲外アクセスエラー: " << e.what() << std::endl;
    }
}

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    accessVectorElement(vec, 10); // 範囲外アクセス
    return 0;
}

演習問題 2: カスタム例外クラスの作成

独自のカスタム例外クラスNegativeValueExceptionを作成し、負の値が入力された場合にこの例外をスローして捕捉するプログラムを書いてください。

#include <iostream>
#include <stdexcept>

// TODO: カスタム例外クラスNegativeValueExceptionを作成する

void checkPositive(int value) {
    // TODO: 負の値の場合にNegativeValueExceptionをスローする
}

int main() {
    try {
        checkPositive(-5);
    } catch (const std::exception& e) {
        std::cerr << "例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

解答例

#include <iostream>
#include <stdexcept>

class NegativeValueException : public std::exception {
public:
    NegativeValueException(const std::string& message) : message_(message) {}
    const char* what() const noexcept override {
        return message_.c_str();
    }
private:
    std::string message_;
};

void checkPositive(int value) {
    if (value < 0) {
        throw NegativeValueException("負の値が入力されました: " + std::to_string(value));
    }
}

int main() {
    try {
        checkPositive(-5);
    } catch (const NegativeValueException& e) {
        std::cerr << "カスタム例外が捕捉されました: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "一般的な例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

演習問題 3: 再スローの実装

関数functionA内で例外をスローし、functionBでそれを捕捉して再スローし、最終的にmain関数で例外を捕捉して処理するプログラムを書いてください。

#include <iostream>
#include <stdexcept>

void functionA() {
    // TODO: ここで例外をスローする
}

void functionB() {
    try {
        functionA();
    } catch (...) {
        // TODO: ここで例外を再スローする
    }
}

int main() {
    try {
        functionB();
    } catch (const std::exception& e) {
        std::cerr << "例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

解答例

#include <iostream>
#include <stdexcept>

void functionA() {
    throw std::runtime_error("functionAでエラーが発生しました");
}

void functionB() {
    try {
        functionA();
    } catch (const std::exception& e) {
        std::cerr << "functionBで例外を捕捉しましたが再スローします: " << e.what() << std::endl;
        throw; // 例外を再スロー
    }
}

int main() {
    try {
        functionB();
    } catch (const std::exception& e) {
        std::cerr << "main関数で例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

応用例: ファイル読み込みと例外処理

以下のプログラムは、ファイルの読み込みを試み、その過程で発生する可能性のある例外を処理します。

#include <iostream>
#include <fstream>
#include <stdexcept>

void readFile(const std::string& filename) {
    std::ifstream file;
    try {
        file.open(filename);
        if (!file.is_open()) {
            throw std::runtime_error("ファイルを開けませんでした");
        }
        std::string line;
        while (std::getline(file, line)) {
            std::cout << line << std::endl;
        }
    } catch (const std::exception& e) {
        std::cerr << "例外が捕捉されました: " << e.what() << std::endl;
    } finally {
        if (file.is_open()) {
            file.close();
        }
    }
}

int main() {
    try {
        readFile("example.txt");
    } catch (const std::exception& e) {
        std::cerr << "main関数で例外が捕捉されました: " << e.what() << std::endl;
    }
    return 0;
}

このプログラムでは、ファイルを開く操作が失敗した場合に例外をスローし、それを捕捉してエラーメッセージを表示します。また、ファイルを読み終えた後、または例外が発生した場合にファイルを確実に閉じる処理を行っています。

まとめ

本記事では、C++の例外処理における基本的なキーワードであるtry, catch, throwの使い方について詳しく解説しました。具体的な使用例を通じて、例外処理の重要性やその効果的な実装方法について学びました。さらに、カスタム例外クラスの作成や例外の再スローの方法についても説明し、標準ライブラリの例外クラスを活用したエラーハンドリングのベストプラクティスを紹介しました。最後に、演習問題と応用例を通じて、実際にコードを書くことで理解を深める機会を提供しました。

これらの知識を活用して、エラー発生時に適切に対応できる堅牢なC++プログラムを作成してください。例外処理を効果的に用いることで、プログラムの信頼性とユーザー体験が向上し、デバッグや保守が容易になります。

コメント

コメントする

目次
  1. 例外処理の基礎
    1. 例外処理の重要性
    2. 例外処理の基本構造
  2. tryブロックの使い方
    1. tryブロックの基本構造
    2. 具体的な例
    3. 複数のtryブロック
  3. catchブロックの使い方
    1. catchブロックの基本構造
    2. 具体的な例
    3. 複数のcatchブロック
    4. catchブロックでの例外処理
  4. throwキーワードの使い方
    1. throwキーワードの基本構造
    2. 具体的な例
    3. 例外オブジェクトの種類
    4. カスタム例外の作成
    5. 例外の再スロー
  5. ネストされたtry-catchブロック
    1. 基本的なネストされたtry-catchブロックの構造
    2. 具体的な例
    3. 複数レベルの例外処理
  6. カスタム例外クラスの作成
    1. カスタム例外クラスの基本構造
    2. カスタム例外クラスの使用例
    3. カスタム例外クラスの拡張
  7. 例外の再スロー
    1. 基本的な再スローの構造
    2. 具体的な例
    3. 再スローの用途
    4. 再スローの注意点
  8. 標準ライブラリの例外クラス
    1. 標準例外クラスの概要
    2. 具体的な使用例
    3. 標準例外クラスの階層構造
    4. 標準例外クラスの使用の利点
  9. 例外処理のベストプラクティス
    1. 適切な例外の使用
    2. 例外の種類を使い分ける
    3. 例外メッセージの明確化
    4. リソースの管理
    5. catchブロックでの具体的な例外の捕捉
    6. 例外の再スローを慎重に行う
    7. 無駄なcatchブロックを避ける
  10. 演習問題と応用例
    1. 演習問題 1: 範囲外アクセスの例外処理
    2. 演習問題 2: カスタム例外クラスの作成
    3. 演習問題 3: 再スローの実装
    4. 応用例: ファイル読み込みと例外処理
  11. まとめ