C++ での例外処理

C++Beginner

はじめに

この実験では、C++ における例外処理の基本概念を学びます。まず、基本的な例外を投げたりキャッチしたりする方法を理解します。これは、プログラム内のランタイムエラーや予期しない状況を処理するための手段です。次に、trycatch、および throw キーワードの使用方法と、std::exception などの標準例外クラスを調べます。また、カスタム例外クラスを定義し、異なる例外タイプに対して複数の catch ブロックを実装する方法を学びます。最後に、ネストされた try-catch ブロックを使用して、プログラムの異なるレベルで例外を処理する方法を調べます。

基本的な例外を投げるとキャッチする

このステップでは、C++ における例外処理の基本概念を学びます。特に、基本的な例外を投げたりキャッチしたりする方法に焦点を当てます。例外は、プログラム内のランタイムエラーや予期しない状況を処理するための手段です。

WebIDE を開き、~/project ディレクトリに新しいファイル basic_exceptions.cpp を作成します。

touch ~/project/basic_exceptions.cpp

basic_exceptions.cpp に次のコードを追加します。

#include <iostream>
#include <stdexcept>

int divide(int numerator, int denominator) {
    // 分母がゼロの場合、例外を投げる
    if (denominator == 0) {
        throw std::runtime_error("Division by zero is not allowed!");
    }
    return numerator / denominator;
}

int main() {
    try {
        // 通常の割り算を試みる
        int result1 = divide(10, 2);
        std::cout << "10 / 2 = " << result1 << std::endl;

        // ゼロで割る試み
        int result2 = divide(10, 0);
        std::cout << "This line will not be executed" << std::endl;
    }
    catch (const std::exception& e) {
        // 例外をキャッチして処理する
        std::cout << "Error: " << e.what() << std::endl;
    }

    return 0;
}

重要なコンポーネントを分解してみましょう。

  1. throw キーワード:

    • エラーが発生したときに例外を生成するために使用されます。
    • この例では、ゼロで割ろうとしたときに std::runtime_error を投げています。
  2. try ブロック:

    • 例外を生成する可能性のあるコードを含みます。
    • リスクのある操作を試みることができます。
    • 例外が発生すると、プログラムの制御は catch ブロックに移ります。
  3. catch ブロック:

    • try ブロックで投げられた例外を処理します。
    • 特定の型の例外をキャッチします(ここでは std::exception)。
    • e.what() を使用してエラーメッセージを取得します。

プログラムをコンパイルして実行します。

g++ basic_exceptions.cpp -o basic_exceptions
./basic_exceptions

実行結果の例:

10 / 2 = 5
Error: Division by zero is not allowed!

基本的な例外処理に関する要点:

  • 例外は、ランタイムエラーを円滑に処理するための手段を提供します。
  • throw は例外を生成します。
  • trycatch は協働して例外状況を管理します。
  • 制御されたエラー処理を提供することで、プログラムのクラッシュを防ぎます。

try、catch、および throw キーワードを理解する

このステップでは、C++ における例外処理の 3 つの重要なキーワード:trycatch、および throw について、さらに深く掘り下げます。これらのキーワードは協働して、プログラムに強力なエラーハンドリングメカニズムを作成します。

WebIDE を開き、~/project ディレクトリに新しいファイル exception_keywords.cpp を作成します。

touch ~/project/exception_keywords.cpp

exception_keywords.cpp に次のコードを追加します。

#include <iostream>
#include <string>

// 例外を投げる可能性のある関数
int processAge(int age) {
    // 無効な年齢の場合、例外を投げる
    if (age < 0) {
        throw std::string("Age cannot be negative");
    }
    if (age > 120) {
        throw std::string("Age is unrealistically high");
    }
    return age;
}

int main() {
    // 最初の try ブロック:年齢の検証を処理する
    try {
        // 成功ケース
        int validAge = processAge(25);
        std::cout << "Valid age: " << validAge << std::endl;

        // これは例外を投げます
        int invalidAge1 = processAge(-5);
        std::cout << "This line will not be executed" << std::endl;
    }
    catch (const std::string& errorMessage) {
        // 投げられた例外を処理するための catch ブロック
        std::cout << "Error: " << errorMessage << std::endl;
    }

    // 2 番目の try ブロック:別の例
    try {
        // これは別の例外を投げます
        int invalidAge2 = processAge(150);
        std::cout << "This line will also not be executed" << std::endl;
    }
    catch (const std::string& errorMessage) {
        std::cout << "Error: " << errorMessage << std::endl;
    }

    return 0;
}

重要なコンポーネントを分解してみましょう。

  1. throw キーワード:

    • 特定の条件が満たされたときに例外を生成するために使用されます。
    • さまざまな型のオブジェクト(文字列、整数、カスタムオブジェクト)を投げることができます。
    • 直ちに現在の関数の実行を停止します。
  2. try ブロック:

    • 例外を生成する可能性のあるコードを含みます。
    • リスクのある操作を試みることができます。
    • 例外が発生すると、プログラムの制御は一致する catch ブロックに移ります。
  3. catch ブロック:

    • 特定の型の例外をキャッチして処理します。
    • 異なる例外タイプに対して複数の catch ブロックを持つことができます。
    • エラーを適切に処理することで、プログラムのクラッシュを防ぎます。

プログラムをコンパイルして実行します。

g++ exception_keywords.cpp -o exception_keywords
./exception_keywords

実行結果の例:

Valid age: 25
Error: Age cannot be negative
Error: Age is unrealistically high

例外キーワードに関する要点:

  • throw はエラー条件を示します。
  • try は例外を生成する可能性のあるコードブロックを定義します。
  • catch は例外を処理し、プログラムの終了を防ぎます。
  • 例外は、ランタイムエラーを管理するための構造化された方法を提供します。

標準例外クラス (std::exception) を使用する

このステップでは、C++ における標準例外クラスについて学び、std::exception 階層を使用してさまざまな種類のランタイムエラーを処理する方法を学びます。C++ 標準ライブラリは、さまざまなエラーシナリオをカバーする事前定義済みの例外クラスのセットを提供しています。

WebIDE を開き、~/project ディレクトリに新しいファイル standard_exceptions.cpp を作成します。

touch ~/project/standard_exceptions.cpp

standard_exceptions.cpp に次のコードを追加します。

#include <iostream>
#include <stdexcept>
#include <limits>

double divideNumbers(double numerator, double denominator) {
    // std::runtime_error を使用してゼロでの割り算をチェックする
    if (denominator == 0) {
        throw std::runtime_error("Division by zero is not allowed!");
    }
    return numerator / denominator;
}

void checkArrayIndex(int* arr, int size, int index) {
    // std::out_of_range を使用して範囲外アクセスをチェックする
    if (index < 0 || index >= size) {
        throw std::out_of_range("Array index is out of bounds!");
    }
    std::cout << "Value at index " << index << ": " << arr[index] << std::endl;
}

int main() {
    try {
        // ゼロでの割り算の例外を示す
        std::cout << "Attempting division:" << std::endl;
        double result = divideNumbers(10, 0);
    }
    catch (const std::runtime_error& e) {
        std::cout << "Runtime Error: " << e.what() << std::endl;
    }

    try {
        // 配列インデックスが範囲外の例外を示す
        int numbers[] = {1, 2, 3, 4, 5};
        int arraySize = 5;

        std::cout << "\nAccessing array elements:" << std::endl;
        checkArrayIndex(numbers, arraySize, 2);  // 有効なインデックス
        checkArrayIndex(numbers, arraySize, 10); // 無効なインデックス
    }
    catch (const std::out_of_range& e) {
        std::cout << "Out of Range Error: " << e.what() << std::endl;
    }

    return 0;
}

標準例外クラスを調べてみましょう。

  1. std::exception

    • すべての標準例外の基底クラス
    • エラーの説明を取得するための仮想 what() メソッドを提供する
  2. 一般的な派生例外クラス:

    • std::runtime_error:プログラム実行中にのみ検出できるランタイムエラー用
    • std::out_of_range:インデックスまたは反復子が有効範囲外の場合
    • その他の一般的なクラスには、std::logic_errorstd::invalid_argument などがあります。

プログラムをコンパイルして実行します。

g++ standard_exceptions.cpp -o standard_exceptions
./standard_exceptions

実行結果の例:

Attempting division:
Runtime Error: Division by zero is not allowed!

Accessing array elements:
Value at index 2: 3
Out of Range Error: Array index is out of bounds!

標準例外クラスに関する要点:

  • さまざまな種類のエラーを処理するための構造化された方法を提供する
  • 各例外クラスは特定の目的を果たす
  • what() メソッドは説明的なエラーメッセージを返す
  • より堅牢で情報の豊富なエラーハンドリングの作成に役立つ

カスタム例外クラスを定義する

このステップでは、C++ で独自のカスタム例外クラスを作成する方法を学びます。カスタム例外を使用すると、アプリケーションの独自の要件に合わせて特定のエラータイプを定義できます。

WebIDE を開き、~/project ディレクトリに新しいファイル custom_exceptions.cpp を作成します。

touch ~/project/custom_exceptions.cpp

custom_exceptions.cpp に次のコードを追加します。

#include <iostream>
#include <string>
#include <stdexcept>

// 銀行口座エラー用のカスタム例外クラス
class InsufficientFundsException : public std::runtime_error {
public:
    // 口座残高と引き出し金額を受け取るコンストラクタ
    InsufficientFundsException(double balance, double amount)
        : std::runtime_error("Insufficient funds"),
          currentBalance(balance),
          withdrawalAmount(amount) {}

    // 詳細なエラー情報を取得するメソッド
    double getCurrentBalance() const { return currentBalance; }
    double getWithdrawalAmount() const { return withdrawalAmount; }

private:
    double currentBalance;
    double withdrawalAmount;
};

class BankAccount {
private:
    double balance;

public:
    BankAccount(double initialBalance) : balance(initialBalance) {}

    void withdraw(double amount) {
        // 引き出し金額が現在の残高を超えるかどうかをチェックする
        if (amount > balance) {
            throw InsufficientFundsException(balance, amount);
        }
        balance -= amount;
        std::cout << "Withdrawal successful. Remaining balance: $"
                  << balance << std::endl;
    }

    double getBalance() const { return balance; }
};

int main() {
    try {
        // 初期残高で銀行口座を作成する
        BankAccount account(100.0);

        // 有効な引き出しを試みる
        account.withdraw(50.0);

        // 無効な引き出しを試みる
        account.withdraw(75.0);
    }
    catch (const InsufficientFundsException& e) {
        std::cout << "Error: " << e.what() << std::endl;
        std::cout << "Current Balance: $" << e.getCurrentBalance() << std::endl;
        std::cout << "Attempted Withdrawal: $" << e.getWithdrawalAmount() << std::endl;
    }

    return 0;
}

カスタム例外クラスを分解してみましょう。

  1. std::runtime_error から継承する:

    • カスタム例外クラスのベースを提供します。
    • エラーの説明を取得するために what() メソッドを使用できるようにします。
  2. カスタム例外の機能:

    • 追加のエラーコンテキストを持つコンストラクタ
    • 特定のエラー詳細を取得するメソッド
    • より情報の豊富なエラーハンドリングを提供します。

プログラムをコンパイルして実行します。

g++ custom_exceptions.cpp -o custom_exceptions
./custom_exceptions

実行結果の例:

Withdrawal successful. Remaining balance: $50
Error: Insufficient funds
Current Balance: $50
Attempted Withdrawal: $75

カスタム例外クラスに関する要点:

  • 標準例外クラスから継承する
  • 特定のエラーコンテキストとメソッドを追加する
  • より詳細なエラー情報を提供する
  • エラーハンドリングとデバッグを改善する

異なる例外タイプに対して複数の catch ブロックを実装する

このステップでは、異なる catch ブロックを使用して複数の例外タイプを処理する方法を学びます。このアプローチにより、プログラムで発生する可能性のあるさまざまな種類の例外に対して、特定のエラーハンドリングを行うことができます。

WebIDE を開き、~/project ディレクトリに新しいファイル multiple_catch.cpp を作成します。

touch ~/project/multiple_catch.cpp

multiple_catch.cpp に次のコードを追加します。

#include <iostream>
#include <stdexcept>
#include <string>

class InvalidAgeException : public std::runtime_error {
public:
    InvalidAgeException(int age)
        : std::runtime_error("Invalid age"), invalidAge(age) {}

    int getInvalidAge() const { return invalidAge; }

private:
    int invalidAge;
};

class Student {
private:
    std::string name;
    int age;

public:
    void setStudent(const std::string& studentName, int studentAge) {
        // 名前の長さを検証する
        if (studentName.length() < 2) {
            throw std::length_error("Name is too short");
        }

        // 年齢の範囲を検証する
        if (studentAge < 0 || studentAge > 120) {
            throw InvalidAgeException(studentAge);
        }

        name = studentName;
        age = studentAge;
    }

    void displayInfo() const {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

int main() {
    Student student;

    // 最初の試み:短い名前
    try {
        student.setStudent("A", 25);
    }
    catch (const std::length_error& e) {
        std::cout << "Length Error: " << e.what() << std::endl;
    }
    catch (const InvalidAgeException& e) {
        std::cout << "Age Error: " << e.what()
                  << " (" << e.getInvalidAge() << ")" << std::endl;
    }

    // 2 番目の試み:無効な年齢
    try {
        student.setStudent("John Doe", 150);
    }
    catch (const std::length_error& e) {
        std::cout << "Length Error: " << e.what() << std::endl;
    }
    catch (const InvalidAgeException& e) {
        std::cout << "Age Error: " << e.what()
                  << " (" << e.getInvalidAge() << ")" << std::endl;
    }

    // 成功した試み
    try {
        student.setStudent("Alice", 20);
        student.displayInfo();
    }
    catch (const std::length_error& e) {
        std::cout << "Length Error: " << e.what() << std::endl;
    }
    catch (const InvalidAgeException& e) {
        std::cout << "Age Error: " << e.what()
                  << " (" << e.getInvalidAge() << ")" << std::endl;
    }

    return 0;
}

複数の catch ブロックを分解してみましょう。

  1. 複数の catch ブロック:

    • 異なる種類の例外を処理することができます。
    • 最も特定的なものから最も一般的なものまでの順序で実行されます。
    • 各ブロックは特定の例外タイプを処理できます。
  2. 例外処理戦略:

    • 最初の catch ブロックは std::length_error を処理します。
    • 2 番目の catch ブロックは InvalidAgeException を処理します。
    • 異なるシナリオに対して特定のエラーメッセージを提供します。

プログラムをコンパイルして実行します。

g++ multiple_catch.cpp -o multiple_catch
./multiple_catch

実行結果の例:

Length Error: Name is too short
Age Error: Invalid age (150)
Name: Alice, Age: 20

複数の catch ブロックに関する要点:

  • 異なる例外タイプを個別に処理する
  • 各例外に対して特定のエラーハンドリングを提供する
  • 例外をキャッチする際に順序が重要である
  • より細かいエラー管理を可能にする

ネストされた try-catch ブロックを使用する

このステップでは、複雑なエラーシナリオを処理し、より細かい例外処理を行うためにネストされた try-catch ブロックをどのように使用するかを学びます。ネストされた try-catch ブロックを使うと、コードの異なるレベルで例外を処理できます。

WebIDE を開き、~/project ディレクトリに新しいファイル nested_exceptions.cpp を作成します。

touch ~/project/nested_exceptions.cpp

nested_exceptions.cpp に次のコードを追加します。

#include <iostream>
#include <stdexcept>
#include <string>

class FileReadError : public std::runtime_error {
public:
    FileReadError(const std::string& filename)
        : std::runtime_error("Error reading file"), fileName(filename) {}

    std::string getFileName() const { return fileName; }

private:
    std::string fileName;
};

class DataProcessor {
public:
    void processFile(const std::string& filename) {
        try {
            // ファイル読み取りをシミュレートする
            if (filename.empty()) {
                throw FileReadError("Empty filename");
            }

            std::cout << "Reading file: " << filename << std::endl;

            try {
                // データ処理をシミュレートする
                validateData(filename);
            }
            catch (const std::runtime_error& e) {
                std::cout << "Inner try-catch: Data validation error" << std::endl;
                std::cout << "Error details: " << e.what() << std::endl;

                // 例外を外側の catch ブロックに再投げる
                throw;
            }
        }
        catch (const FileReadError& e) {
            std::cout << "Outer try-catch: File read error" << std::endl;
            std::cout << "Filename: " << e.getFileName() << std::endl;
        }
        catch (...) {
            std::cout << "Caught unknown exception" << std::endl;
        }
    }

private:
    void validateData(const std::string& filename) {
        // データ検証をシミュレートする
        if (filename == "corrupt.txt") {
            throw std::runtime_error("Corrupt file data");
        }
        std::cout << "Data validation successful" << std::endl;
    }
};

int main() {
    DataProcessor processor;

    // シナリオ 1: 空のファイル名
    std::cout << "Scenario 1: Empty Filename" << std::endl;
    processor.processFile("");

    // シナリオ 2: 破損したファイル
    std::cout << "\nScenario 2: Corrupt File" << std::endl;
    processor.processFile("corrupt.txt");

    // シナリオ 3: 正常なファイル
    std::cout << "\nScenario 3: Valid File" << std::endl;
    processor.processFile("data.txt");

    return 0;
}

ネストされた try-catch ブロックを分解してみましょう。

  1. 外側の try-catch ブロック:

    • ファイルレベルの例外を処理します。
    • FileReadError やその他の潜在的なエラーをキャッチします。
  2. 内側の try-catch ブロック:

    • データ処理固有の例外を処理します。
    • 例外を外側の catch ブロックに再投げることができます。
  3. 全てキャッチするハンドラ (catch (...)):

    • 予期しない例外をキャッチします。
    • エラー処理の最終層を提供します。

プログラムをコンパイルして実行します。

g++ nested_exceptions.cpp -o nested_exceptions
./nested_exceptions

実行結果の例:

Scenario 1: Empty Filename
Outer try-catch: File read error
Filename: Empty filename

Scenario 2: Corrupt File
Reading file: corrupt.txt
Inner try-catch: Data validation error
Error details: Corrupt file data

Scenario 3: Valid File
Reading file: data.txt
Data validation successful

ネストされた try-catch ブロックに関する要点:

  • 複数レベルの例外処理を提供します。
  • より詳細なエラー管理を可能にします。
  • 例外を外側の catch ブロックに再投げることができます。
  • 複雑なエラーシナリオに役立ちます。

まとめ

この実験では、C++ における例外処理の基本概念を学びました。まず、throwtrycatch キーワードを使って基本的な例外を投げる方法とキャッチする方法を理解しました。ゼロでの割り算エラーを処理するために std::runtime_error 例外クラスの使用方法を調べました。また、catch ブロック内で e.what() 関数を使ってエラーメッセージにアクセスする方法を学びました。これらの技術は、ランタイムエラーを円滑に処理し、プログラムのクラッシュを防ぎ、より堅牢で信頼性の高い C++ アプリケーションを書くための方法を提供します。