C++のthrowは、プログラムの実行中に発生したエラーや異常な状態を「例外」として通知するために使用するキーワードです。
throwによって投げられた例外は、tryとcatchを利用して捕捉し、適切なエラー処理を行えます。
ファイルの読み込みに失敗した場合や、不正な引数が渡された場合、処理を正常に続行できない場合などに利用されます。
ここでは、C++のthrowの基本的な使い方から、try・catchとの関係、標準例外、再スロー、スタックアンワインディング、noexceptとの違いまで詳しく解説します。
C++のthrowとは
C++のthrowは、例外を発生させるために使用するキーワードです。
基本的な例外処理では、主に次の3つを組み合わせます。
try {
// 例外が発生する可能性のある処理
}
catch (...) {
// 例外が発生した場合の処理
}
それぞれの役割は次のとおりです。
| キーワード | 役割 |
|---|---|
try | 例外が発生する可能性のある処理を囲む |
throw | 例外を発生させる |
catch | 発生した例外を捕捉して処理する |
なお、throwは必ずtryブロックの中に直接記述する必要があるわけではありません。
tryブロックから呼び出された関数の中でthrowしても、対応するcatchまで例外を伝播させることができます。
throwの基本的な使い方
throwで例外を投げる
基本的な構文は次のとおりです。
throw 式;
例えば、整数値を例外として投げる場合は次のように記述できます。
#include <iostream>
int main() {
try {
throw 100;
}
catch (int e) {
std::cout << "例外を捕捉しました: " << e << '\n';
}
return 0;
}
実行すると、次のように表示されます。
例外を捕捉しました: 100
throw 100;が実行されると、通常の処理はその地点で中断されます。
その後、投げられた例外に適合するcatchが検索されます。
throwの後にある通常処理は実行されない
次のコードを見てみましょう。
#include <iostream>
int main() {
try {
std::cout << "処理開始\n";
throw 1;
std::cout << "この処理は実行されません\n";
}
catch (int e) {
std::cout << "例外処理\n";
}
}
実行結果は次のようになります。
処理開始
例外処理
throw式が評価されると、その時点で通常の制御フローが中断され、例外処理が開始されます。
ただし、単純に即座にcatchへジャンプするわけではありません。
途中に破棄すべきローカルオブジェクトが存在する場合には、デストラクタを呼び出しながら呼び出し元へ戻っていきます。
この仕組みを「スタックアンワインディング」といいます。
try・throw・catchの関係
基本的な例外処理
例えば、0で割る処理を防ぐ関数を考えてみましょう。
#include <iostream>
#include <stdexcept>
double divide(double a, double b) {
if (b == 0.0) {
throw std::invalid_argument("0で割ることはできません");
}
return a / b;
}
int main() {
try {
double result = divide(10.0, 0.0);
std::cout << result << '\n';
}
catch (const std::invalid_argument& e) {
std::cout << "エラー: " << e.what() << '\n';
}
return 0;
}
bが0.0の場合、次の処理が実行されます。
throw std::invalid_argument("0で割ることはできません");
その後、std::invalid_argument型の例外を処理できるcatchが検索されます。
throwは別の関数内から実行できる
例外は、tryブロックの中で直接発生させる必要はありません。
#include <iostream>
#include <stdexcept>
void process() {
throw std::runtime_error("処理に失敗しました");
}
int main() {
try {
process();
}
catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
}
この場合、process()の中で発生した例外がmain()まで伝播し、catchで捕捉されます。
標準例外クラスをthrowする方法
C++では整数や文字列など、さまざまな型を例外として投げることができます。
例えば、次のコードも文法的には有効です。
throw 10;
ただし、実際のプログラムではstd::exceptionを基底とする例外クラスを利用するのが一般的です。
std::runtime_error
std::runtime_errorは、実行時に発生する一般的なエラーを表現するために使用できます。
#include <stdexcept>
throw std::runtime_error("処理に失敗しました");
例えば次のように使用できます。
#include <iostream>
#include <stdexcept>
void process() {
throw std::runtime_error("データの処理に失敗しました");
}
int main() {
try {
process();
}
catch (const std::runtime_error& e) {
std::cerr << e.what() << '\n';
}
}
std::invalid_argument
std::invalid_argumentは、関数に渡された引数が不正な場合に利用できます。
#include <stdexcept>
void setAge(int age) {
if (age < 0) {
throw std::invalid_argument("年齢は0以上で指定してください");
}
}
std::out_of_range
std::out_of_rangeは、添字や値などが有効範囲を超えている場合に利用できます。
throw std::out_of_range("範囲外です");
例えば、std::vector::at()は範囲外の添字が指定された場合にstd::out_of_rangeを投げます。
#include <iostream>
#include <vector>
#include <stdexcept>
int main() {
std::vector<int> values = {10, 20, 30};
try {
std::cout << values.at(10) << '\n';
}
catch (const std::out_of_range& e) {
std::cout << "範囲外アクセス: " << e.what() << '\n';
}
}
std::exceptionとは
標準例外の基底クラス
C++標準ライブラリには、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::system_errorなどがあります。
what()でエラーメッセージを取得する
std::exception系の例外では、what()を利用してエラー内容を取得できます。
try {
// 処理
}
catch (const std::exception& e) {
std::cerr << "エラー: " << e.what() << '\n';
}
この書き方を利用すると、複数の標準例外をまとめて処理しやすくなります。
catchではconst参照で受け取る
const参照が一般的
例外オブジェクトは、一般的にconst参照で受け取ります。
catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
値で受け取ることもできます。
catch (std::exception e) {
}
しかし、値で受け取るとコピーが発生するうえ、派生クラスの情報が失われる「オブジェクトスライシング」が発生する可能性があります。
そのため、基本的には次の形式を使用します。
catch (const std::exception& e)
複数のcatchを使用する方法
例外の種類によって処理を分ける
複数種類の例外が発生する場合には、catchを複数記述できます。
#include <iostream>
#include <stdexcept>
int main() {
try {
throw std::invalid_argument("不正な引数です");
}
catch (const std::invalid_argument& e) {
std::cout << "invalid_argument: " << e.what() << '\n';
}
catch (const std::runtime_error& e) {
std::cout << "runtime_error: " << e.what() << '\n';
}
catch (const std::exception& e) {
std::cout << "その他の標準例外: " << e.what() << '\n';
}
}
派生クラスのcatchを先に記述する
対応するcatchは、記述順に確認されます。
そのため、より具体的な派生クラスを先に記述するのが基本です。
catch (const std::out_of_range& e) {
}
catch (const std::exception& e) {
}
std::exceptionを先に記述すると、派生クラスの例外までそこで捕捉されてしまいます。
catch(…)ですべての例外を捕捉する
型を問わず例外を捕捉できる
C++では、次のように記述すると型を問わず例外を捕捉できます。
catch (...) {
std::cout << "何らかの例外が発生しました\n";
}
例えば次のように使用できます。
try {
throw 123;
}
catch (...) {
std::cout << "例外を捕捉しました\n";
}
ただし、catch (...)では例外オブジェクトの型や内容を直接取得できません。
そのため、通常は最後の安全網として利用します。
try {
// 処理
}
catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
catch (...) {
std::cerr << "未知の例外が発生しました\n";
}
throw;による例外の再スロー
現在の例外をそのまま投げ直す
catchで捕捉した例外を、さらに上位の呼び出し元へ伝えたい場合があります。
その場合には、値を書かずにthrow;と記述します。
throw;
例えば次のように使用します。
#include <iostream>
#include <stdexcept>
void process() {
try {
throw std::runtime_error("エラー発生");
}
catch (const std::exception& e) {
std::cout << "process内: " << e.what() << '\n';
throw;
}
}
int main() {
try {
process();
}
catch (const std::exception& e) {
std::cout << "main内: " << e.what() << '\n';
}
}
process()では一度例外を捕捉しています。
その後、throw;で元の例外を再スローし、main()で改めて捕捉しています。
throw;とthrow e;は異なる
再スローでは次の形式を使用することが重要です。
throw;
次のような書き方は、通常の再スローとは意味が異なります。
catch (const std::exception& e) {
throw e;
}
throw e;では、式eから新しい例外オブジェクトが作られます。
そのため、元の例外が派生クラスだった場合、派生型の情報が失われる可能性があります。
元の例外をそのまま再スローしたい場合は、throw;を使用します。
なお、現在処理中の例外が存在しない状態でthrow;を実行すると、std::terminate()が呼び出されます。
独自の例外クラスをthrowする
std::runtime_errorなどを継承する
C++では独自の例外クラスを作成できます。
一般的には、std::exceptionやstd::runtime_errorなどを継承します。
#include <iostream>
#include <stdexcept>
#include <string>
class DatabaseError : public std::runtime_error {
public:
explicit DatabaseError(const std::string& message)
: std::runtime_error(message) {
}
};
void connectDatabase() {
throw DatabaseError("データベースに接続できません");
}
int main() {
try {
connectDatabase();
}
catch (const DatabaseError& e) {
std::cerr << "DBエラー: " << e.what() << '\n';
}
}
独自例外を作成すると、エラーの種類を明確に分類できます。
大規模なプログラムでは、例外クラスにエラーコードなどの追加情報を持たせる場合もあります。
関数内でthrowを使用する
不正な引数を検出する
throwは、関数内で異常な入力や状態を検出した際に利用されることがよくあります。
#include <stdexcept>
double calculateAverage(int total, int count) {
if (count <= 0) {
throw std::invalid_argument("countは1以上である必要があります");
}
return static_cast<double>(total) / count;
}
呼び出し側では次のように処理できます。
try {
double average = calculateAverage(100, 0);
}
catch (const std::invalid_argument& e) {
std::cerr << e.what() << '\n';
}
この設計では、関数側は異常が発生したことだけを通知し、実際にどう対処するかは呼び出し側で決められます。
例外が関数をまたいで伝播する仕組み
呼び出し元へ例外が伝わる
次のような関数呼び出しを考えます。
main()
↓
functionA()
↓
functionB()
functionB()で例外が発生し、途中で対応するcatchが存在しなければ、例外は呼び出し元へ伝播していきます。
#include <iostream>
#include <stdexcept>
void functionB() {
throw std::runtime_error("functionBでエラー");
}
void functionA() {
functionB();
}
int main() {
try {
functionA();
}
catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
}
この例では、functionB()で投げられた例外がfunctionA()を通過し、最終的にmain()のcatchで処理されます。
スタックアンワインディングとは
例外発生時にデストラクタが呼ばれる
例外が呼び出し元へ伝播する際には、途中のスコープを抜けながら構築済みのローカルオブジェクトが破棄されます。
この処理をスタックアンワインディングといいます。
#include <iostream>
#include <stdexcept>
class Sample {
public:
~Sample() {
std::cout << "デストラクタが呼ばれました\n";
}
};
void test() {
Sample sample;
throw std::runtime_error("エラー");
}
int main() {
try {
test();
}
catch (const std::exception& e) {
std::cout << e.what() << '\n';
}
}
test()が例外によって終了する場合でも、sampleのデストラクタは適切に呼び出されます。
throwとRAIIの関係
生のリソース管理には注意する
次のように手動でメモリを確保している場合を考えます。
void process() {
int* data = new int[100];
throw std::runtime_error("エラー");
delete[] data;
}
throwによって関数を抜けるため、delete[] data;は実行されません。
その結果、メモリリークが発生します。
RAIIを利用する
現代的なC++では、std::vectorやスマートポインタなどを使用してリソースを自動管理します。
#include <stdexcept>
#include <vector>
void process() {
std::vector<int> data(100);
throw std::runtime_error("エラー");
}
例外が発生しても、スタックアンワインディングによってstd::vectorのデストラクタが呼び出されます。
そのため、内部のメモリも自動的に解放されます。
このようなリソース管理方法をRAIIといいます。
C++で例外を使用する場合、RAIIを意識した設計は非常に重要です。
例外をcatchしなかった場合
std::terminate()が呼び出される
投げられた例外に対応するcatchが最終的に見つからなかった場合、std::terminate()が呼び出されます。
#include <stdexcept>
int main() {
throw std::runtime_error("エラー");
}
このコードには例外を捕捉するcatchがありません。
そのため、例外が未処理となり、最終的にプログラムは終了します。
なお、未捕捉例外によってstd::terminate()が呼び出されるまでにスタックアンワインディングを実行するかどうかについては、実装に依存する部分があります。
noexcept関数とthrowの関係
noexceptとは
C++11以降では、関数から例外を外部へ送出しないことをnoexceptで表現できます。
void func() noexcept {
}
noexcept関数から例外が外部へ伝播しようとすると、std::terminate()が呼び出されます。
#include <stdexcept>
void func() noexcept {
throw std::runtime_error("エラー");
}
この例では、投げられた例外がfunc()の外へ伝播しようとするため、std::terminate()が呼び出されます。
noexcept関数内でもcatchは使える
noexcept関数の内部で例外を発生させること自体が禁止されているわけではありません。
関数内部で捕捉すれば問題ありません。
#include <stdexcept>
void func() noexcept {
try {
throw std::runtime_error("エラー");
}
catch (...) {
// この関数内で例外を処理する
}
}
重要なのは、例外をnoexcept関数の外へ漏らさないことです。
古いthrow例外指定との違い
throw(Type)は現在使用しない
以前のC++では、関数が送出する可能性のある例外型を次のように指定する記法がありました。
void func() throw(std::runtime_error);
このような非空の動的例外指定は、C++11で非推奨となり、C++17で削除されました。
現在のC++コードでは使用しません。
throw()も古い記法
次のような書き方も存在しました。
void func() throw();
throw()は例外を送出しないことを示すために使われていた記法です。
C++17ではnoexcept(true)と同等の意味として扱われ、C++17で非推奨となり、C++20で削除されています。
現代のC++では次のように記述します。
void func() noexcept {
}
整理すると次のようになります。
| 記法 | 扱い |
|---|---|
throw(Type) | C++11で非推奨、C++17で削除 |
throw() | C++17で非推奨、C++20で削除 |
noexcept | C++11以降で利用する現代的な記法 |
なお、ここで説明している例外送出のthrow 式;や再スローのthrow;は、現在のC++でも利用されます。
デストラクタからthrowする際の注意点
基本的には例外を外へ出さない
デストラクタから例外を外部へ伝播させる設計は、原則として避けるのが一般的です。
特に、別の例外によるスタックアンワインディングの途中でデストラクタからさらに例外が外へ伝播すると、std::terminate()が呼び出されます。
そのため、デストラクタでは例外を外部へ漏らさないように設計することが重要です。
後処理で発生したエラーを報告する必要がある場合には、明示的なclose()などの関数を用意する設計も検討できます。
throwを使うべき場面
通常処理を継続できない異常を通知する
throwは、通常の処理をそのまま継続することが難しい状況を呼び出し元へ通知する場合に適しています。
例えば、次のようなケースがあります。
- 関数に不正な引数が渡された場合
- ファイルやデータの処理に失敗した場合
- オブジェクトを正常に構築できない場合
- 深い関数階層から上位へエラーを通知したい場合
- 現在の関数では適切な復旧方法を判断できない場合
特にコンストラクタは通常の戻り値を返せないため、初期化に失敗したことを通知する方法として例外が利用されることがあります。
throwを使わない方がよい場面
通常の条件分岐に例外を使わない
throwは通常の制御フローを表現するための機能ではありません。
例えば、次のような処理は通常の条件分岐で十分です。
if (age >= 18) {
// 成人向け処理
} else {
// 未成年向け処理
}
このような正常な条件分岐を例外で実装する必要はありません。
optionalなどが適している場合もある
例えば、「検索したが見つからなかった」という結果が通常起こり得るのであれば、例外ではなくstd::optionalを利用する方法があります。
#include <optional>
std::optional<int> findValue() {
return std::nullopt;
}
また、エラーの詳細を戻り値で扱いたい場合には、C++23で標準化されたstd::expectedなどを利用する設計もあります。
エラー処理の方法には、主に次のような選択肢があります。
- 例外
std::optionalstd::expectedstd::error_code- 戻り値によるステータス通知
どの方法を使用するかは、APIの性質や設計方針によって異なります。
throwを使う際のポイント
基本的な注意点を押さえる
C++でthrowを使用する場合は、次の点を意識すると安全で分かりやすいコードになります。
| ポイント | 基本的な考え方 |
|---|---|
| 投げる例外 | std::exception系の例外クラスが一般的 |
| 捕捉方法 | const参照で受け取る |
| 再スロー | throw;を使用する |
| リソース管理 | RAIIを利用する |
生のnew・delete | 可能な限り避ける |
| 通常の条件分岐 | ifなどを利用する |
| 例外を送出しない関数 | 必要に応じてnoexceptを利用する |
| 独自例外 | 標準例外クラスから派生させる方法が一般的 |
基本形としては、次のようなコードを覚えておくと分かりやすいでしょう。
#include <iostream>
#include <stdexcept>
void process(int value) {
if (value < 0) {
throw std::invalid_argument("valueは0以上で指定してください");
}
std::cout << "value = " << value << '\n';
}
int main() {
try {
process(-1);
}
catch (const std::exception& e) {
std::cerr << "エラー: " << e.what() << '\n';
}
return 0;
}
C++のthrowについてのまとめ
C++のthrowは、プログラム内で発生したエラーや異常な状態を例外として呼び出し元へ通知するために使用します。
基本的には、try、throw、catchを組み合わせて例外処理を実装します。
throwが実行されると通常の処理が中断され、対応するcatchが検索されます。
その過程ではスタックアンワインディングが行われ、スコープを抜けるオブジェクトのデストラクタが適切に呼び出されます。
実際のC++プログラムでは、std::runtime_errorやstd::invalid_argumentなど、std::exception系の例外クラスを投げる方法が一般的です。
また、例外を捕捉するときはconst参照を使い、同じ例外を再スローするときはthrow;を使用します。
例外処理を安全に利用するためには、std::vectorやスマートポインタなどを活用したRAIIによるリソース管理も重要です。
一方で、throwは通常の条件分岐を置き換えるためのものではありません。
正常に起こり得る結果については、std::optionalやstd::expectedなどを利用した方が適している場合もあります。
throwの仕組みだけでなく、例外を使うべき場面と使わない方がよい場面まで理解することで、より安全で保守しやすいC++プログラムを作成できるようになります。
以上、C++のthrowの使い方についてでした。
最後までお読みいただき、ありがとうございました。
