ファイルが存在しない、ゼロ除算が起きた、配列の範囲外にアクセスした――こうしたエラー事象は、プログラムが予期していない「例外的な事態」です。プログラミングにおける「例外」とは、通常の処理の流れでは対処できない“異常事態”を通知するための仕組みです。
C++のthrow
とcatch
は、例外処理のための仕組みで、プログラムの信頼性と保守性を高めるために非常に便利です。
本稿ではまず、通常の分岐によるエラー処理を示し、try/throw/catchによる例外処理を説明します。
通常の分岐によるエラー処理
以下のC++コードは、整数の配列をループ処理し、各値を2倍にする関数を呼び出すという構造になっています。ただし、負の値が含まれている場合は処理を拒否し、エラーメッセージを表示するという例外的な分岐も含まれています。
#include <iostream> #include <vector> // valueをチェックし0以上なら2倍した値をresultにtrueを bool process(int value, int& result) { if (value < 0) { std::cerr << "エラー: 負の値は処理できません(値: " << value << ")\n"; return false; } result = value * 2; return true; } int main() { std::vector<int> data = {1, 2, -3, 4, -5}; for (int i = 0; i < data.size(); ++i) { int output; if (!process(data[i], output)) { std::cerr << "処理失敗(index: " << i << ")\n"; continue; } std::cout << "処理成功: " << output << "\n"; } return 0; }
コード全体の処理概要
data
という整数のベクター(配列)をループで走査- 各値を
process()
関数で処理(2倍にする) - 負の値は処理せず、エラーとして扱う
- 成功した場合は結果を表示、失敗した場合はエラーメッセージを表示
関数 process
は何をしているか
bool process(int value, int& result) { if (value < 0) { std::cerr << "エラー: 負の値は処理できません(値: " << value << ")\n"; return false; } result = value * 2; return true; }
処理説明
この関数は「値を2倍にするが、負の値は拒否する」という明確なルールを持っています。
行 | 要素 | 説明 |
---|---|---|
1 | value (引数:1) | 入力値(処理対象) |
1 | result (引数:2) | 処理結果を格納する参照変数 |
2 | value < 0 | 負の値は処理対象外とする条件 |
3 | std::cerr | 標準エラー出力にメッセージを表示 |
4 | return false | 処理失敗を通知 |
6 | result = value * 2 | 正常時の処理(2倍) |
7 | return true | 処理成功を通知 |
main()
関数のループ処理
std::vector<int> data = {1, 2, -3, 4, -5}; for (int i = 0; i < data.size(); ++i) { int output; if (!process(data[i], output)) { std::cerr << "処理失敗(index: " << i << ")\n"; continue; } std::cout << "処理成功: " << output << "\n"; }
処理の説明
data
に格納された整数を1つずつ取り出すprocess()
関数に渡して処理false
が返ってきた場合はエラーとして扱い、continue
で次のループへ- 成功した場合は
output
に結果が入り、標準出力に表示
実行結果(Ubuntu 24.04LTS)
処理成功: 2 処理成功: 4 エラー: 負の値は処理できません(値: -3) 処理失敗(index: 2) 処理成功: 8 エラー: 負の値は処理できません(値: -5) 処理失敗(index: 4)
このコードは「例外を使わずに、戻り値と分岐でエラー処理を行う」典型的なスタイルです。もしこれを例外処理(throw/catch)スタイルに書き換えるとどうなるかを比較するための具体的なコードを次に示します。
try/throw/catchによる例外処理
try/throw/catchを使用して書き直したコードの全体は下の通りです。
#include <iostream> #include <vector> #include <stdexcept> int process(int value) { if (value < 0) { throw std::invalid_argument("負の値は処理できません(値: " + std::to_string(value) + ")"); } return value * 2; } int main() { std::vector<int> data = {1, 2, -3, 4, -5}; for (int i = 0; i < data.size(); ++i) { try { int output = process(data[i]); std::cout << "処理成功: " << output << "\n"; } catch (const std::invalid_argument& e) { std::cerr << "例外発生(index: " << i << "): " << e.what() << "\n"; } } return 0; }
ここでは、例外を用いたエラー処理のC++コードを解説します。負の値に対して例外を投げ、呼び出し元のループ側で捕捉して処理を続行する構造です。
ヘッダーのインクルード
#include <iostream>
- 役割: 標準入出力(
std::cout
,std::cerr
)を使うためのヘッダー。 - 補足: 画面への出力やエラー出力に使用。
#include <vector>
- 役割: 可変長配列コンテナ
std::vector
を使うためのヘッダー。 - 補足: 動的にサイズが変わる配列で、今回は入力データ保持に使用。
#include <stdexcept>
- 役割: 標準例外クラス群(
std::invalid_argument
など)の宣言。 - 補足: 例外オブジェクトの型として利用する。
関数 process の定義
int process(int value) {
- 役割: 入力
value
を処理し、結果(2倍)を返す関数。 - 設計: エラーは戻り値ではなく例外で通知する。
if (value < 0) {
- 役割: 負の値を想定外入力として検出し弾く。
throw std::invalid_argument("負の値は処理できません(値: " + std::to_string(value) + ")");
- 役割: 不正な引数を示す
std::invalid_argument
を投げる。 - 詳細: メッセージ生成に
std::to_string(value)
を用い、エラーメッセージに具体的な値を埋め込む。throw
によりこの関数は即座に終了し、呼び出し側の対応するcatch
まで制御が巻き戻る(スタックアンワインド)。
main 関数(データ準備とループ)
std::vector<int> data = {1, 2, -3, 4, -5};
- 役割: 処理対象データの初期化。
- 意図: 正常値と異常値(負)を混在させ、例外処理の妥当性を検証可能にする。
for (int i = 0; i < data.size(); ++i) {
- 役割: 処理対象データを走査。
- 補足:
data.size()
はsize_t
(符号なし)だが、ここでは単純化のためint
と比較している。厳密にはsize_t
を使うとより安全。
例外を伴う処理と例外の捕捉
try {
- 役割: 例外が起きうる処理を囲むブロックの開始。
- 意図: 例外をループ内で個別に処理し、例外が発生しても、それ以外データ要素の処理継続を可能にする。
int output = process(data[i]);
- 役割:
process
を呼び出し、成功時は結果を受け取る。 - 挙動:
data[i]
が負の場合、この行で例外が送出され、直ちにcatch
へジャンプする(以降の同ブロック内文は実行されない)。
std::cout << "処理成功: " << output << "\n";
- 役割: 正常終了時の結果表示。
- 補足: 例外が発生した場合、この行はスキップされる。
} catch (const std::invalid_argument& e) {
- 役割:
std::invalid_argument
型(およびその派生型)の例外を捕捉。 - 設計: 具体的な例外型で受けることで、異常内容ごとに分岐してハンドリング可能。
std::cerr << "例外発生(index: " << i << "): " << e.what() << "\n";
- 役割: エラー内容を標準エラーへ出力。
- 詳細:
e.what()
は例外メッセージ(throw
時に渡した文字列)をconst char*
として返す。
実行時の流れと想定出力のテスト仕様とは
- 正の値(1, 2, 4):
process
が 2倍を返し、「処理成功: 2」「処理成功: 4」「処理成功: 8」を表示。 - 負の値(-3, -5):
process
がstd::invalid_argument
を投げ、対応するcatch
が「例外発生(index: 2/4): 負の値は処理できません(値: -3/-5)」をstd::cerr
に出力。 - 処理: 異常な入力は例外で分離して扱いながら、ループは止まらずに残りの要素を処理し続ける。
実行結果
処理成功: 2 処理成功: 4 例外発生(index: 2): 負の値は処理できません(値: -3) 処理成功: 8 例外発生(index: 4): 負の値は処理できません(値: -5)
分岐と例外の違いとは
異なる5つの観点から、分岐とthrow/catchによる例外処理の違いをまとめると、以下の通りです。
観点 | 分岐 | throw/catch |
---|---|---|
制御の明示性 | 高い(if文で明示) | 低め(例外が非同期的に飛ぶ。process()の中から例外が投げられることを知っている必要がある) |
コードの簡潔さ | 冗長になりがち | すっきり書ける |
パフォーマンス | 高速(例外処理なし) | 例外発生時にオーバーヘッドがあるとされている |
保守性 | 分岐が多いと煩雑 | 例外の型で整理しやすい |
RAII(メモリの開放)との相性 | 手動管理が必要 | 自動解放がしやすい |
さいごに
どちらも一長一短ですが、ループ中の関数呼び出しで異常値を扱うという文脈では、例外処理の方がロジックの本質に集中できるという利点があります。