第1章:例外処理

はじめに

CPP Module 05では、C++の例外処理を学びます。例外処理は、エラーを検出した場所と処理する場所を分離し、堅牢なプログラムを作成するための重要な機能です。

---

1. 例外処理の歴史

1.1 エラー処理の進化

初期のC言語:

  • 戻り値でエラーを示す(-1, NULL, errno)
  • 呼び出し側で毎回チェックが必要
  • エラーチェックを忘れやすい

FILE* fp = fopen("file.txt", "r");
if (fp == NULL) {
    perror("fopen failed");
    return -1;
}

C++の例外処理:

  • エラーをthrowで投げる
  • エラー処理をcatchでまとめて行う
  • エラーチェックを忘れると例外が伝播
  • try {
        File f("file.txt");
        f.read();
    } catch (const FileException& e) {
        std::cerr << e.what() << std::endl;
    }
    

    1.2 例外処理の起源

    例外処理の概念は、PL/I(1964年)で初めて導入されました。C++はCLU(1975年、Barbara Liskov)とAdaの影響を受けて例外処理を採用しました。

    1.3 C++での採用

    Bjarne Stroustrupは1989年にC++に例外処理を追加しました。設計の目標は:

  • エラー処理コードを正常コードから分離
  • エラーを無視できないようにする
  • スタックアンワインドによるリソース解放

---

2. try-catch構文

2.1 基本構文

try {
    // 例外が発生する可能性のあるコード
    throw std::runtime_error("Something went wrong");
} catch (const std::exception& e) {
    // 例外を処理
    std::cerr << "Error: " << e.what() << std::endl;
}

2.2 throw文

// 値を投げる
throw 42;
throw "Error message";
throw std::runtime_error("Error");

// オブジェクトを投げる
class MyException : public std::exception {
public:
    const char* what() const throw() {
        return "My custom exception";
    }
};
throw MyException();

2.3 複数のcatch

try {
    // ...
} catch (const std::bad_alloc& e) {
    // メモリ割り当て失敗
} catch (const std::runtime_error& e) {
    // ランタイムエラー
} catch (const std::exception& e) {
    // その他のstd::exception派生
} catch (...) {
    // すべての例外をキャッチ
}

注意: catchは順番に評価されるため、派生クラスを先に書く必要があります。

2.4 例外の再throw

try {
    try {
        throw std::runtime_error("Original error");
    } catch (const std::exception& e) {
        std::cerr << "Logging: " << e.what() << std::endl;
        throw;  // 同じ例外を再throw
    }
} catch (const std::exception& e) {
    std::cerr << "Final handler: " << e.what() << std::endl;
}

---

3. 標準例外クラス

3.1 std::exception階層

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

3.2 std::exceptionインターフェース

class exception {
public:
    virtual ~exception() throw();
    virtual const char* what() const throw();
};

3.3 標準例外の使用

#include <stdexcept>

void validateAge(int age) {
    if (age < 0) {
        throw std::invalid_argument("Age cannot be negative");
    }
    if (age > 150) {
        throw std::out_of_range("Age is out of valid range");
    }
}

---

4. カスタム例外クラス

4.1 std::exceptionを継承

class GradeTooHighException : public std::exception {
public:
    const char* what() const throw() {
        return "Grade is too high";
    }
};

class GradeTooLowException : public std::exception {
public:
    const char* what() const throw() {
        return "Grade is too low";
    }
};

4.2 ネストされた例外クラス

42の課題では、例外クラスをクラス内にネストします:

class Bureaucrat {
public:
    class GradeTooHighException : public std::exception {
    public:
        const char* what() const throw() {
            return "Grade is too high";
        }
    };

    class GradeTooLowException : public std::exception {
    public:
        const char* what() const throw() {
            return "Grade is too low";
        }
    };

private:
    std::string _name;
    int _grade;  // 1 (highest) to 150 (lowest)
};

4.3 例外の使用

Bureaucrat::Bureaucrat(const std::string& name, int grade)
    : _name(name), _grade(grade) {
    if (grade < 1) {
        throw GradeTooHighException();
    }
    if (grade > 150) {
        throw GradeTooLowException();
    }
}

---

5. 例外安全性

5.1 例外安全性のレベル

David Abrahamsによる分類:

| レベル | 保証 | |--------|------| | No-throw | 例外を投げない | | Strong | 失敗時、操作前の状態に戻る | | Basic | 不変条件を維持、リソースリークなし | | No guarantee | 何も保証しない |

5.2 Basic保証の例

void Bureaucrat::incrementGrade() {
    if (_grade <= 1) {
        throw GradeTooHighException();
    }
    _grade--;  // 例外後もオブジェクトは有効な状態
}

5.3 RAIIと例外安全性

class FileHandler {
    FILE* _file;
public:
    FileHandler(const char* filename) : _file(fopen(filename, "r")) {
        if (!_file) throw std::runtime_error("Failed to open file");
    }
    ~FileHandler() {
        if (_file) fclose(_file);
    }
};

void process() {
    FileHandler fh("data.txt");  // 例外時も自動クローズ
    // 処理...
}  // デストラクタでファイルクローズ

---

6. スタックアンワインド

6.1 スタックアンワインドとは

例外が投げられると、catchに到達するまでスタックフレームが巻き戻されます。この過程で、ローカルオブジェクトのデストラクタが呼ばれます。

void func3() {
    LocalObject obj3;
    throw std::runtime_error("Error in func3");
}

void func2() {
    LocalObject obj2;
    func3();
}

void func1() {
    LocalObject obj1;
    func2();
}

int main() {
    try {
        func1();
    } catch (const std::exception& e) {
        // obj3, obj2, obj1 のデストラクタが呼ばれた後ここに到達
    }
}

6.2 注意点

デストラクタから例外を投げない:

スタックアンワインド中にデストラクタが例外を投げると、std::terminate()が呼ばれます。

class Bad {
public:
    ~Bad() {
        throw std::runtime_error("Don't do this!");  // 危険!
    }
};

---

7. 42課題での実装ポイント

7.1 Bureaucratクラス(ex00)

class Bureaucrat {
public:
    class GradeTooHighException : public std::exception {
        const char* what() const throw();
    };
    class GradeTooLowException : public std::exception {
        const char* what() const throw();
    };

private:
    const std::string _name;
    int _grade;  // 1-150

public:
    Bureaucrat(const std::string& name, int grade);
    Bureaucrat(const Bureaucrat& other);
    Bureaucrat& operator=(const Bureaucrat& other);
    ~Bureaucrat();

    const std::string& getName() const;
    int getGrade() const;
    void incrementGrade();  // may throw
    void decrementGrade();  // may throw
};

std::ostream& operator<<(std::ostream& os, const Bureaucrat& b);

7.2 Formクラス(ex01)

class Form {
public:
    class GradeTooHighException : public std::exception { /* ... */ };
    class GradeTooLowException : public std::exception { /* ... */ };

private:
    const std::string _name;
    bool _signed;
    const int _gradeToSign;
    const int _gradeToExecute;

public:
    Form(const std::string& name, int gradeToSign, int gradeToExecute);
    // ...
    void beSigned(const Bureaucrat& bureaucrat);  // may throw
};

7.3 具象Formクラス(ex02)

class AForm {  // 抽象クラス
protected:
    virtual void executeAction() const = 0;

public:
    void execute(const Bureaucrat& executor) const;  // may throw
};

class ShrubberyCreationForm : public AForm { /* ... */ };
class RobotomyRequestForm : public AForm { /* ... */ };
class PresidentialPardonForm : public AForm { /* ... */ };

7.4 Internクラス(ex03)

class Intern {
public:
    AForm* makeForm(const std::string& formName, const std::string& target);
};

---

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

8.1 値でthrow、参照でcatch

// 良い
throw MyException();
catch (const MyException& e) { }

// 避ける
throw new MyException();  // メモリリークの可能性
catch (MyException* e) { }

8.2 例外仕様(C++11以前)

// C++03スタイル(非推奨)
void func() throw(MyException);

// C++11以降
void func() noexcept;        // 例外を投げない
void func() noexcept(false); // 例外を投げる可能性

8.3 例外を使うべきでない場面

  • 正常なフロー制御
  • 予測可能で頻繁なエラー(オプショナル値を使う)
  • 即座にリカバリできるエラー
  • ---

    まとめ

    本章で学んだこと:

  • try-catch構文: 例外のthrowとcatch
  • 標準例外: std::exception階層
  • カスタム例外: ネストされた例外クラス
  • 例外安全性: Basic/Strong/No-throw保証
  • スタックアンワインド: デストラクタの自動呼び出し
  • ベストプラクティス: 値でthrow、参照でcatch

次章では、各演習の詳細な実装を解説します。