C++のstd::exceptionは、標準ライブラリで使われる例外クラス階層の基底クラスです。
簡単に言うと、std::runtime_error、std::logic_error、std::bad_allocなど、多くの標準例外をまとめて扱うための共通インターフェースです。
代表的には、次のように使います。
#include <iostream>
#include <exception>
int main() {
try {
// 何らかの処理
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
}
std::exceptionにはwhat()という仮想関数があり、例外の説明文字列を取得できます。
virtual const char* what() const noexcept;
そのため、std::exceptionの派生クラスであれば、catch (const std::exception& e)でまとめて捕捉し、e.what()でエラー内容を表示できます。
ただし、std::exceptionは「直接投げるための便利な例外型」というより、標準例外を多態的に扱うための基底クラスと考える方が適切です。
std::exceptionの基本的な形
std::exceptionは、概念的には次のようなクラスです。
namespace std {
class exception {
public:
exception() noexcept;
exception(const exception&) noexcept;
exception& operator=(const exception&) noexcept;
virtual ~exception() noexcept;
virtual const char* what() const noexcept;
};
}
実際の実装は処理系やC++のバージョンによって異なりますが、重要なのは次の2点です。
1つ目は、what()が仮想関数であることです。
これにより、基底クラスであるstd::exception&で受け取っても、実際の派生クラスに応じたメッセージを取得できます。
2つ目は、デストラクタが仮想デストラクタであることです。これにより、std::exceptionは基底クラスとして安全に使える設計になっています。
what()の役割
what()は例外の説明を返す
what()は、例外の内容を表す文字列を返す関数です。
#include <iostream>
#include <stdexcept>
int main() {
try {
throw std::runtime_error("ファイルを開けませんでした");
} catch (const std::exception& e) {
std::cout << e.what() << std::endl;
}
}
出力例は次のようになります。
ファイルを開けませんでした
ここではstd::runtime_errorを投げていますが、catchではstd::exception&として受け取っています。
std::runtime_errorはstd::exceptionの派生クラスなので、基底クラスの参照で捕捉できます。
そして、what()は仮想関数なので、実際の例外型に応じたメッセージが返されます。
what()はnoexceptである
what()は次のようにnoexcept指定されています。
virtual const char* what() const noexcept;
noexceptは、「この関数は例外を外へ投げない」という意味です。
例外処理中にエラーメッセージを取得しようとして、さらにwhat()が例外を投げると、例外処理そのものが不安定になります。
そのため、what()は例外を投げない設計になっています。
自作例外クラスでwhat()をオーバーライドする場合も、基本的には次のように書きます。
const char* what() const noexcept override
catch (const std::exception& e)で受け取る理由
C++では、標準例外を捕捉するときに次の形がよく使われます。
catch (const std::exception& e)
この書き方には、いくつか重要な理由があります。
参照で受け取ることでスライシングを防げる
例外を値渡しで受け取ると、派生クラスの情報が失われる可能性があります。
これをオブジェクトスライシングと呼びます。
避けたい例です。
try {
throw std::runtime_error("error");
} catch (std::exception e) {
std::cout << e.what() << std::endl;
}
この場合、std::runtime_errorとして投げた例外が、std::exception部分だけに切り詰められる可能性があります。
そのため、通常は参照で受け取ります。
try {
throw std::runtime_error("error");
} catch (const std::exception& e) {
std::cout << e.what() << std::endl;
}
constを付けることで不要な変更を防げる
例外オブジェクトは、通常、捕捉後に変更する必要がありません。
そのため、const参照で受け取ります。
catch (const std::exception& e)
what()はconstメンバ関数なので、const std::exception&からでも問題なく呼び出せます。
基底クラスで受け取ることで標準例外をまとめて扱える
std::runtime_error、std::invalid_argument、std::out_of_rangeなど、多くの標準例外はstd::exceptionを継承しています。
そのため、次のように書くことで、多くの標準例外をまとめて捕捉できます。
try {
// 何らかの処理
} catch (const std::exception& e) {
std::cerr << "標準例外: " << e.what() << std::endl;
}
これは、実務でもよく使われる基本形です。
代表的な標準例外クラス
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
│
├── std::bad_alloc
├── std::bad_cast
├── std::bad_typeid
└── std::bad_exception
これはあくまで代表例です。C++標準ライブラリには、ほかにも次のような例外クラスがあります。
std::system_error
std::ios_base::failure
std::filesystem::filesystem_error
std::bad_optional_access
std::bad_variant_access
std::bad_function_call
std::regex_error
std::future_error
したがって、上記の階層は「主要な標準例外の一部」として理解するとよいです。
std::logic_error系の例外
std::logic_errorは、主にプログラムのロジック上の誤りを表す例外です。
たとえば、呼び出し側の使い方が間違っている、引数が不正である、前提条件を満たしていない、といったケースで使われます。
std::invalid_argument
std::invalid_argumentは、不正な引数が渡されたときに使います。
#include <stdexcept>
void setAge(int age) {
if (age < 0) {
throw std::invalid_argument("年齢は0以上である必要があります");
}
}
この例では、年齢に負の値が渡された場合にstd::invalid_argumentを投げています。
std::out_of_range
std::out_of_rangeは、範囲外アクセスなどを表す例外です。
#include <iostream>
#include <vector>
#include <stdexcept>
int main() {
std::vector<int> v = {1, 2, 3};
try {
std::cout << v.at(10) << std::endl;
} catch (const std::out_of_range& e) {
std::cout << "範囲外アクセス: " << e.what() << std::endl;
}
}
std::vector::at()は範囲チェックを行います。
範囲外の要素にアクセスしようとすると、std::out_of_rangeを投げます。
一方、operator[]は範囲チェックを保証しません。
v.at(10); // 範囲外なら std::out_of_range を投げる
v[10]; // 範囲外なら未定義動作
安全性を重視する場面では、at()を使うことで範囲外アクセスを例外として扱えます。
std::length_error
std::length_errorは、文字列やコンテナなどで許容される最大サイズを超えようとした場合に使われます。
throw std::length_error("文字列が長すぎます");
自分のコードで、サイズ制限を超えたことを明示的に表したい場合にも使えます。
std::runtime_error系の例外
std::runtime_errorは、主に実行時に発生する問題を表す例外です。
たとえば、ファイルが存在しない、通信に失敗した、設定ファイルの読み込みに失敗した、外部サービスがエラーを返した、などです。
std::runtime_error
std::runtime_errorは、汎用的な実行時エラーを表す例外です。
#include <stdexcept>
#include <fstream>
#include <string>
void loadFile(const std::string& path) {
std::ifstream file(path);
if (!file) {
throw std::runtime_error("ファイルを開けませんでした");
}
}
このように、プログラムのロジックそのものというより、実行時の環境や状態によって発生する問題に使います。
ただし、たとえば「空のファイルパスが引数として渡された」というケースは、std::runtime_errorよりもstd::invalid_argumentの方が自然な場合があります。
void loadFile(const std::string& path) {
if (path.empty()) {
throw std::invalid_argument("ファイルパスが空です");
}
std::ifstream file(path);
if (!file) {
throw std::runtime_error("ファイルを開けませんでした");
}
}
呼び出し側の入力が不正なのか、実行時環境による失敗なのかを意識して使い分けるとよいです。
std::overflow_error
std::overflow_errorは、オーバーフローを表すための標準例外です。
throw std::overflow_error("計算結果が大きすぎます");
ただし、C++の組み込み整数演算が自動的にstd::overflow_errorを投げるわけではありません。
たとえば、次のような符号付き整数のオーバーフローは、通常std::overflow_errorではなく未定義動作になります。
int x = INT_MAX;
int y = x + 1; // std::overflow_error が自動的に投げられるわけではない
そのため、std::overflow_errorは、ライブラリや自分のコードで明示的に投げる例外型として理解するのがよいです。
std::underflow_error
std::underflow_errorは、アンダーフローを表すための標準例外です。
throw std::underflow_error("計算結果が小さすぎます");
こちらも、組み込み演算が常に自動的に投げるものではありません。
必要に応じて、自分のコードやライブラリ側で明示的に使う例外型です。
catchの順番に注意する
複数のcatchを書く場合は、具体的な派生クラスを先に書き、一般的な基底クラスを後に書く必要があります。
悪い例
try {
throw std::out_of_range("範囲外です");
} catch (const std::exception& e) {
std::cout << "std::exception: " << e.what() << std::endl;
} catch (const std::out_of_range& e) {
std::cout << "out_of_range: " << e.what() << std::endl;
}
このコードでは、std::out_of_rangeはstd::exceptionの派生クラスなので、先にstd::exception側で捕捉されてしまいます。
そのため、後ろのstd::out_of_rangeのcatchには到達しません。
良い例
try {
throw std::out_of_range("範囲外です");
} catch (const std::out_of_range& e) {
std::cout << "out_of_range: " << e.what() << std::endl;
} catch (const std::exception& e) {
std::cout << "std::exception: " << e.what() << std::endl;
}
このように、より具体的な例外を先に、より一般的な例外を後に書きます。
実務では、次のような構成がよく使われます。
try {
// 処理
} catch (const std::out_of_range& e) {
// 範囲外アクセスだけ個別対応
} catch (const std::invalid_argument& e) {
// 不正な引数だけ個別対応
} catch (const std::exception& e) {
// その他の標準例外
}
catch (...)との違い
catch (...)は、型を問わずC++例外を捕捉するための構文です。
try {
// ...
} catch (...) {
std::cout << "何らかの例外が発生しました" << std::endl;
}
std::exceptionを継承していない例外も捕捉できます。
たとえば、C++では次のような値を投げることも技術的には可能です。
throw 1;
throw "error";
これらはstd::exception派生ではないため、次のcatchでは捕捉できません。
try {
throw 1;
} catch (const std::exception& e) {
// ここには来ない
} catch (...) {
// ここに来る
}
ただし、catch (...)が標準C++として捕捉できるのは、C++の例外機構で投げられた例外です。
セグメンテーションフォルトやOS固有の例外などを、標準C++として必ず捕捉できるという意味ではありません。
実務では、最後の保険として次のように使うことがあります。
try {
// アプリケーション本体
} catch (const std::exception& e) {
std::cerr << "error: " << e.what() << std::endl;
} catch (...) {
std::cerr << "unknown error" << std::endl;
}
std::exceptionを直接投げるべきか
std::exceptionは抽象クラスではありません。
そのため、次のように直接投げることはできます。
throw std::exception();
しかし、実務ではあまりおすすめされません。
理由は、エラー内容が具体的ではないからです。
std::exceptionのwhat()が返す内容は処理系依存であり、具体的な原因を伝えるには不十分です。
そのため、実際には次のような具体的な例外型を使う方が一般的です。
throw std::runtime_error("設定ファイルの読み込みに失敗しました");
throw std::invalid_argument("user_idが空です");
throw std::out_of_range("indexが範囲外です");
特に注意したいのは、次の書き方です。
throw std::exception("error");
これは標準C++では使えません。
標準のstd::exceptionには、文字列メッセージを受け取るコンストラクタがないためです。
一部の処理系、たとえばMSVCでは非標準拡張としてコンパイルできる場合がありますが、移植性のあるC++コードとしては避けるべきです。
メッセージ付きの例外を投げたい場合は、次のようにstd::runtime_errorなどを使います。
throw std::runtime_error("error");
自作例外クラスを作る方法
自作例外クラスを作る場合、std::exceptionを直接継承することもできます。
ただし、実務ではstd::runtime_errorやstd::logic_errorなど、既存の標準例外クラスを継承する方が簡単です。
std::runtime_errorを継承する例
#include <stdexcept>
#include <string>
class FileOpenError : public std::runtime_error {
public:
explicit FileOpenError(const std::string& message)
: std::runtime_error(message) {}
};
使い方は次のとおりです。
#include <iostream>
int main() {
try {
throw FileOpenError("設定ファイルを開けませんでした");
} catch (const FileOpenError& e) {
std::cout << "FileOpenError: " << e.what() << std::endl;
} catch (const std::exception& e) {
std::cout << "std::exception: " << e.what() << std::endl;
}
}
この方法では、what()を自分で実装する必要がありません。
std::runtime_errorがメッセージ文字列の管理を行ってくれるため、自作例外として扱いやすい形になります。
std::exceptionを直接継承する例
std::exceptionを直接継承する場合は、what()を自分でオーバーライドします。
#include <exception>
class MyException : public std::exception {
public:
const char* what() const noexcept override {
return "MyExceptionが発生しました";
}
};
この例では、固定文字列を返しているため問題ありません。
ただし、次のような実装は危険です。
#include <exception>
#include <string>
class MyException : public std::exception {
public:
const char* what() const noexcept override {
std::string message = "error";
return message.c_str(); // 危険
}
};
messageはwhat()関数のローカル変数です。
関数が終了すると破棄されるため、message.c_str()で返したポインタは無効になります。
安全にするには、メッセージをメンバ変数として保持します。
#include <exception>
#include <string>
#include <utility>
class MyException : public std::exception {
private:
std::string message_;
public:
explicit MyException(std::string message)
: message_(std::move(message)) {}
const char* what() const noexcept override {
return message_.c_str();
}
};
ただし、このように自前でメッセージ管理を実装するより、通常はstd::runtime_errorを継承する方が簡単です。
class MyException : public std::runtime_error {
public:
explicit MyException(const std::string& message)
: std::runtime_error(message) {}
};
例外を再送出する方法
捕捉した例外を上位に投げ直したい場合は、throw;を使います。
try {
// 何らかの処理
} catch (const std::exception& e) {
std::cerr << "ログ出力: " << e.what() << std::endl;
throw;
}
ここで注意したいのは、次のように書かないことです。
catch (const std::exception& e) {
throw e; // 推奨されない
}
throw e;と書くと、eという式を新しい例外として投げることになります。
このとき、eの静的型がstd::exceptionであれば、派生クラスの情報が失われる可能性があります。
一方、throw;は現在処理中の例外をそのまま再送出します。
catch (const std::exception& e) {
throw; // 現在の例外をそのまま再送出
}
例外の型情報を保ちたい場合は、必ずthrow;を使います。
noexcept関数と例外
noexcept指定された関数から例外が外へ出ると、std::terminate()が呼ばれます。
#include <stdexcept>
void f() noexcept {
throw std::runtime_error("error"); // 外へ出ると std::terminate()
}
より正確には、throwした瞬間に必ず終了するというより、その例外がnoexcept関数の外へ出ようとしたときにstd::terminate()が呼ばれます。
関数内で捕捉する場合は問題ありません。
void f() noexcept {
try {
throw std::runtime_error("error");
} catch (...) {
// 関数の外へ出ないのでOK
}
}
noexceptは、ムーブコンストラクタやデストラクタなど、例外を外へ出してはいけない場面で重要です。
ただし、noexceptを付けた関数内で例外が発生する可能性がある場合は、その関数内で適切に捕捉する必要があります。
実務でよく使う例外処理の形
アプリケーションのエントリポイントでは、未処理の例外をまとめて捕捉することがあります。
#include <iostream>
#include <exception>
int main() {
try {
// アプリケーション本体
} catch (const std::exception& e) {
std::cerr << "Unhandled exception: " << e.what() << std::endl;
return 1;
} catch (...) {
std::cerr << "Unknown exception" << std::endl;
return 1;
}
return 0;
}
この形にしておくと、std::exception派生の例外はwhat()で内容を表示でき、非標準的なC++例外もcatch (...)で最低限捕捉できます。
ただし、catch (...)では例外の詳細な型情報やメッセージを取得できないため、基本的にはstd::exception派生の例外を投げる設計にしておく方が扱いやすいです。
例外型の使い分け
標準例外は、エラーの性質に応じて使い分けるとコードの意図が分かりやすくなります。
| 状況 | 使う例外の例 |
|---|---|
| 引数が不正 | std::invalid_argument |
| 範囲外アクセス | std::out_of_range |
| プログラムの前提条件違反 | std::logic_error系 |
| ファイルや通信などの実行時エラー | std::runtime_error |
| メモリ確保失敗 | std::bad_alloc |
| 型変換失敗 | std::bad_cast |
| OSやAPIのエラーコードと紐付けたい | std::system_error |
| ファイルシステム系の詳細なエラー | std::filesystem::filesystem_error |
| 独自ドメインのエラー | std::runtime_errorやstd::logic_errorを継承した自作例外 |
たとえば、次のように考えると使い分けやすいです。
呼び出し側の使い方が間違っているなら、std::logic_error系です。
throw std::invalid_argument("引数が不正です");
実行時の外部環境や状態によって失敗したなら、std::runtime_error系です。
throw std::runtime_error("ファイルを開けませんでした");
配列やコンテナの範囲外なら、std::out_of_rangeです。
throw std::out_of_range("indexが範囲外です");
ヘッダーの違い
例外クラスによって、必要なヘッダーが異なります。
代表的なものは次のとおりです。
| 例外クラス | 主なヘッダー |
|---|---|
std::exception | <exception> |
std::runtime_error | <stdexcept> |
std::logic_error | <stdexcept> |
std::invalid_argument | <stdexcept> |
std::out_of_range | <stdexcept> |
std::bad_alloc | <new> |
std::bad_cast | <typeinfo> |
std::bad_typeid | <typeinfo> |
std::bad_exception | <exception> |
std::system_error | <system_error> |
std::filesystem::filesystem_error | <filesystem> |
std::runtime_errorやstd::invalid_argumentを使う場合は、基本的に<stdexcept>をインクルードします。
#include <stdexcept>
std::exceptionだけを使う場合は、<exception>です。
#include <exception>
std::exceptionについての重要ポイント
std::exceptionについて押さえておきたいポイントは、次のとおりです。
std::exceptionは、標準例外の共通基底クラスです。
catch (const std::exception& e)
という形で、多くの標準例外をまとめて捕捉できます。
what()を使うと、例外の説明文字列を取得できます。
std::cerr << e.what() << std::endl;
例外は値ではなく、基本的にconst参照で受け取ります。
catch (const std::exception& e)
複数のcatchを書く場合は、派生クラスを先に、基底クラスを後に書きます。
catch (const std::out_of_range& e) {
// 具体的な例外
} catch (const std::exception& e) {
// 一般的な標準例外
}
捕捉した例外をそのまま再送出する場合は、throw e;ではなくthrow;を使います。
catch (const std::exception& e) {
throw;
}
std::exceptionを直接投げることはできますが、情報量が少ないため、通常はstd::runtime_errorやstd::invalid_argumentなどの具体的な例外を使います。
throw std::runtime_error("実行時エラー");
throw std::invalid_argument("不正な引数");
throw std::out_of_range("範囲外アクセス");
まとめ
std::exceptionは、C++標準例外の基底クラスです。
主な役割は、標準例外をまとめて扱うための共通インターフェースを提供することです。
特に重要なのは、次の基本形です。
try {
// 処理
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
この書き方により、std::runtime_error、std::invalid_argument、std::out_of_rangeなど、多くの標準例外をまとめて捕捉できます。
ただし、実際に例外を投げるときは、std::exceptionを直接投げるより、エラーの内容に応じた具体的な例外型を使う方が適切です。
throw std::runtime_error("ファイルを開けませんでした");
throw std::invalid_argument("引数が不正です");
throw std::out_of_range("indexが範囲外です");
自作例外を作る場合も、まずはstd::runtime_errorやstd::logic_errorを継承する方法を検討するとよいです。
class MyError : public std::runtime_error {
public:
explicit MyError(const std::string& message)
: std::runtime_error(message) {}
};
std::exceptionは「具体的なエラー内容を表すためのクラス」というより、C++の例外処理において標準例外を共通的に扱うための土台です。
そのため、catch (const std::exception& e)で受け取り、what()で内容を確認する、という基本形をしっかり押さえておくことが重要です。
以上、C++のstd::exceptionについてでした。
最後までお読みいただき、ありがとうございました。
