第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
---
まとめ
本章で学んだこと:
次章では、各演習の詳細な実装を解説します。