第10章:例外処理
はじめに
例外処理は、エラーが発生した場所とそれを処理する場所を分離するメカニズムです。この概念は、CLU(1975年)で最初に導入され、C++は1990年に例外処理を採用しました。
本章では、例外処理の理論的背景から、C++での実践的な使用方法、そして例外安全性について学びます。
---
1. 例外処理の概念
1.1 なぜ例外処理が必要か
従来のエラー処理方法には問題がありました:
戻り値によるエラー通知:
// C言語スタイル
FILE* file = fopen("data.txt", "r");
if (file == NULL) {
// エラー処理
return -1;
}
// ファイル処理...
問題点:
- 戻り値がエラーと正常値で競合する
- エラーチェックを忘れやすい
- エラー処理コードが本質的なコードに混在
例外処理のアプローチ:
// C++スタイル
try {
std::ifstream file("data.txt");
if (!file) {
throw std::runtime_error("Failed to open file");
}
// ファイル処理...
} catch (const std::runtime_error& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
利点:
- エラー処理と正常処理の分離
- エラーを無視しにくい
- スタックの巻き戻しでリソースが自動解放(RAII)
1.2 例外の歴史
| 年 | 言語/出来事 | 内容 | |-----|------------|------| | 1975 | CLU | 例外処理の概念を導入 | | 1985 | C++ | 当初は例外なし | | 1990 | C++ 2.0 | 例外処理を追加 | | 1998 | C++98 | 例外仕様(throw()) | | 2011 | C++11 | noexcept導入、例外仕様非推奨化 |
1.3 基本的な構文
// 例外を投げる
void riskyFunction() {
if (/* エラー条件 */) {
throw std::runtime_error("Something went wrong");
}
}
// 例外をキャッチする
int main() {
try {
riskyFunction();
// 正常処理
} catch (const std::runtime_error& e) {
std::cerr << "Runtime error: " << e.what() << std::endl;
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
} catch (...) {
std::cerr << "Unknown exception" << std::endl;
}
return 0;
}
---
2. 標準例外クラス
2.1 例外階層
C++標準ライブラリの例外階層:
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
│
├── std::bad_alloc (new失敗)
├── std::bad_cast (dynamic_cast失敗)
├── std::bad_typeid (typeid失敗)
└── std::bad_exception
2.2 各例外クラスの使い分け
#include <stdexcept>
void validateAge(int age) {
if (age < 0 || age > 150) {
throw std::invalid_argument("Age must be between 0 and 150");
}
}
void accessElement(const std::vector<int>& vec, size_t index) {
if (index >= vec.size()) {
throw std::out_of_range("Index out of range");
}
}
void divideNumbers(int a, int b) {
if (b == 0) {
throw std::domain_error("Division by zero");
}
}
void checkOverflow(long result) {
if (result > INT_MAX) {
throw std::overflow_error("Integer overflow");
}
}
int main() {
try {
validateAge(-5);
} catch (const std::invalid_argument& e) {
std::cerr << "Invalid argument: " << e.what() << std::endl;
}
try {
std::vector<int> v = {1, 2, 3};
accessElement(v, 10);
} catch (const std::out_of_range& e) {
std::cerr << "Out of range: " << e.what() << std::endl;
}
return 0;
}
2.3 std::exceptionの使い方
void processAll() {
try {
// 様々な例外が発生する可能性
} catch (const std::logic_error& e) {
// プログラムの論理エラー
std::cerr << "Logic error: " << e.what() << std::endl;
} catch (const std::runtime_error& e) {
// 実行時エラー
std::cerr << "Runtime error: " << e.what() << std::endl;
} catch (const std::exception& e) {
// その他の標準例外
std::cerr << "Exception: " << e.what() << std::endl;
} catch (...) {
// 非標準の例外
std::cerr << "Unknown exception" << std::endl;
}
}
---
3. カスタム例外クラス
3.1 基本的なカスタム例外
class FileNotFoundException : public std::runtime_error {
public:
FileNotFoundException(const std::string& filename)
: std::runtime_error("File not found: " + filename),
filename_(filename) {}
const std::string& getFilename() const { return filename_; }
private:
std::string filename_;
};
class DatabaseException : public std::runtime_error {
public:
DatabaseException(const std::string& message, int errorCode)
: std::runtime_error(message),
errorCode_(errorCode) {}
int getErrorCode() const { return errorCode_; }
private:
int errorCode_;
};
void openFile(const std::string& filename) {
std::ifstream file(filename);
if (!file) {
throw FileNotFoundException(filename);
}
}
int main() {
try {
openFile("nonexistent.txt");
} catch (const FileNotFoundException& e) {
std::cerr << e.what() << std::endl;
std::cerr << "Filename: " << e.getFilename() << std::endl;
}
return 0;
}
3.2 例外クラスのベストプラクティス
// ベストプラクティスに従った例外クラス
class ApplicationException : public std::exception {
public:
explicit ApplicationException(const std::string& message)
: message_(message) {}
// 例外安全なwhat()の実装
const char* what() const noexcept override {
return message_.c_str();
}
// コピー可能で例外を投げない
ApplicationException(const ApplicationException&) noexcept = default;
ApplicationException& operator=(const ApplicationException&) noexcept = default;
// 仮想デストラクタ
virtual ~ApplicationException() noexcept = default;
private:
std::string message_;
};
// 派生例外クラス
class NetworkException : public ApplicationException {
public:
NetworkException(const std::string& message, int errorCode)
: ApplicationException(message + " (Error: " + std::to_string(errorCode) + ")"),
errorCode_(errorCode) {}
int getErrorCode() const noexcept { return errorCode_; }
private:
int errorCode_;
};
3.3 例外の再投げ
void lowLevelFunction() {
throw std::runtime_error("Low level error");
}
void midLevelFunction() {
try {
lowLevelFunction();
} catch (const std::exception& e) {
// ログを取る
std::cerr << "Caught in mid level: " << e.what() << std::endl;
// 同じ例外を再投げ
throw; // throw e; ではない!スライシングを避ける
}
}
void highLevelFunction() {
try {
midLevelFunction();
} catch (const std::exception& e) {
std::cerr << "Caught in high level: " << e.what() << std::endl;
}
}
---
4. 例外安全性
4.1 例外安全性のレベル
例外安全性には3つのレベルがあります:
| レベル | 名前 | 保証 | |--------|------|------| | 1 | 基本保証 | リソースリークなし、不変条件維持 | | 2 | 強い保証 | 操作は成功するか、元の状態のまま | | 3 | nothrow保証 | 例外を投げない |
4.2 基本保証
リソースリークがなく、オブジェクトが有効な状態を維持:
class BasicGuarantee {
std::vector<int> data;
public:
void addElement(int value) {
data.push_back(value); // 例外が発生してもdataは有効
}
};
4.3 強い保証
操作が失敗しても、元の状態が完全に保持される:
class StrongGuarantee {
std::vector<int> data;
public:
void replaceData(const std::vector<int>& newData) {
std::vector<int> temp = newData; // コピーを作成(ここで例外発生の可能性)
temp.swap(data); // swapはnoexcept
} // 成功すればnewDataで置換、失敗すれば元のまま
};
// コピーアンドスワップイディオム
class Resource {
int* data;
size_t size;
public:
Resource(size_t n) : data(new int[n]()), size(n) {}
Resource(const Resource& other)
: data(new int[other.size]), size(other.size) {
std::copy(other.data, other.data + size, data);
}
~Resource() { delete[] data; }
// 強い例外安全な代入演算子
Resource& operator=(Resource other) { // 値渡し(コピーコンストラクタ呼び出し)
swap(other); // noexcept
return *this;
}
void swap(Resource& other) noexcept {
std::swap(data, other.data);
std::swap(size, other.size);
}
};
4.4 nothrow保証
class NothrowGuarantee {
int value;
public:
// noexcept指定子
int getValue() const noexcept {
return value;
}
void setValue(int v) noexcept {
value = v;
}
// ムーブ操作はnoexceptであるべき
NothrowGuarantee(NothrowGuarantee&& other) noexcept
: value(other.value) {}
NothrowGuarantee& operator=(NothrowGuarantee&& other) noexcept {
value = other.value;
return *this;
}
};
4.5 RAIIと例外安全性
RAIIはコンストラクタでリソースを取得し、デストラクタで解放するパターンです。例外発生時もデストラクタは呼ばれるため、リソースリークを防げます:
class Lock {
std::mutex& mutex_;
public:
explicit Lock(std::mutex& m) : mutex_(m) {
mutex_.lock();
}
~Lock() {
mutex_.unlock(); // 例外発生時も確実にアンロック
}
// コピー禁止
Lock(const Lock&) = delete;
Lock& operator=(const Lock&) = delete;
};
void criticalSection(std::mutex& m) {
Lock lock(m); // ロック取得
doSomethingThatMightThrow(); // 例外発生しても...
} // 自動的にアンロック
---
5. noexceptキーワード(C++11)
5.1 noexcept指定子
関数が例外を投げないことを宣言:
// 例外を投げない関数
void safeFunction() noexcept {
// 例外を投げないコード
}
// 条件付きnoexcept
template <typename T>
void conditionalNoexcept(T value) noexcept(std::is_nothrow_move_constructible<T>::value) {
// Tのムーブがnoexceptならこの関数もnoexcept
}
// noexceptを違反した場合
void breakNoexcept() noexcept {
throw std::runtime_error("Oops"); // std::terminateが呼ばれる!
}
5.2 noexcept演算子
式がnoexceptかどうかをコンパイル時に判定:
void mayThrow() { throw 1; }
void noThrow() noexcept {}
int main() {
std::cout << std::boolalpha;
std::cout << "mayThrow: " << noexcept(mayThrow()) << std::endl; // false
std::cout << "noThrow: " << noexcept(noThrow()) << std::endl; // true
// 算術演算はnoexcept
std::cout << "1 + 2: " << noexcept(1 + 2) << std::endl; // true
return 0;
}
5.3 ムーブとnoexcept
ムーブ操作がnoexceptでないと、STLコンテナは効率的に動作しません:
class OptimalMove {
int* data;
public:
// noexceptなムーブコンストラクタ
OptimalMove(OptimalMove&& other) noexcept
: data(other.data) {
other.data = nullptr;
}
// noexceptなムーブ代入演算子
OptimalMove& operator=(OptimalMove&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
// vectorのreallocateでムーブが使われる
std::vector<OptimalMove> vec;
vec.push_back(OptimalMove()); // noexceptならムーブ、そうでなければコピー
---
6. 例外処理のベストプラクティス
6.1 投げる例外の選択
// 良い例:適切な例外タイプを選択
void validateInput(const std::string& input) {
if (input.empty()) {
throw std::invalid_argument("Input cannot be empty");
}
}
// 悪い例:一般的すぎる例外
void badValidation(const std::string& input) {
if (input.empty()) {
throw std::exception(); // 情報が少なすぎる
}
}
// 悪い例:非標準の例外
void veryBadValidation(const std::string& input) {
if (input.empty()) {
throw "Input cannot be empty"; // const char*を投げている
}
}
6.2 例外のキャッチ方法
// 良い例:const参照でキャッチ
try {
// ...
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
// 悪い例:値でキャッチ(スライシングの危険)
try {
// ...
} catch (std::exception e) { // コピーが発生、派生クラスの情報が失われる
std::cerr << e.what() << std::endl;
}
6.3 デストラクタでの例外
デストラクタは例外を投げてはいけません:
class Resource {
public:
~Resource() noexcept { // C++11以降、デストラクタは暗黙的にnoexcept
try {
cleanup();
} catch (...) {
// 例外を握りつぶすか、ログを取る
std::cerr << "Cleanup failed" << std::endl;
}
}
private:
void cleanup() {
// 例外を投げる可能性のあるクリーンアップ処理
}
};
6.4 例外と制御フロー
例外は例外的な状況に対してのみ使用:
// 悪い例:通常の制御フローに例外を使用
int findValue(const std::vector<int>& vec, int target) {
for (size_t i = 0; i < vec.size(); ++i) {
if (vec[i] == target) {
throw i; // 例外を戻り値代わりに使用
}
}
throw std::runtime_error("Not found");
}
// 良い例:戻り値を使用
std::optional<size_t> findValue(const std::vector<int>& vec, int target) {
for (size_t i = 0; i < vec.size(); ++i) {
if (vec[i] == target) {
return i;
}
}
return std::nullopt;
}
---
7. 例外 vs エラーコード
7.1 比較
| 観点 | 例外 | エラーコード | |------|------|-------------| | エラー無視 | 困難(伝播する) | 容易(チェックを忘れやすい) | | パフォーマンス | 正常時は影響なし、異常時は遅い | 常に小さなオーバーヘッド | | コードの可読性 | エラー処理が分離 | 本質的なコードに混在 | | コンストラクタ | 使用可能 | 困難(戻り値がない) | | リソース管理 | RAIIで自動化 | 手動管理が必要 |
7.2 使い分けの指針
例外を使う場合:
- コンストラクタでのエラー
- 深いコールスタックを越えたエラー伝播
- エラーが例外的(まれ)な状況
- RAIIを活用したい場合
エラーコードを使う場合:
- 頻繁に発生する「エラー」(入力検証など)
- リアルタイムシステム
- 例外が無効化されている環境
- C言語との互換性が必要
7.3 std::expected(C++23)
例外とエラーコードの中間的なアプローチ:
// C++23 std::expected
#include <expected>
std::expected<int, std::string> divide(int a, int b) {
if (b == 0) {
return std::unexpected("Division by zero");
}
return a / b;
}
int main() {
auto result = divide(10, 2);
if (result) {
std::cout << "Result: " << *result << std::endl;
} else {
std::cout << "Error: " << result.error() << std::endl;
}
return 0;
}
---
まとめ
本章で学んだこと:
次章では、モダンC++(C++11以降)の機能について学びます。
---
練習問題
void transfer(BankAccount& from, BankAccount& to, double amount) {
from.withdraw(amount); // 例外を投げる可能性
to.deposit(amount); // 例外を投げる可能性
}
- カスタム例外クラス
ParseExceptionを作成してください。以下の情報を含むこと:
- 以下のコードの例外安全性の問題を指摘してください:
class Widget {
int* data1;
int* data2;
public:
Widget() : data1(new int(1)), data2(new int(2)) {}
~Widget() { delete data1; delete data2; }
};
- noexceptを適切に使用して、以下のクラスを改善してください:
class Buffer {
char* data;
size_t size;
public:
Buffer(size_t n);
Buffer(const Buffer& other);
Buffer(Buffer&& other);
Buffer& operator=(const Buffer& other);
Buffer& operator=(Buffer&& other);
~Buffer();
void swap(Buffer& other);
};