はじめに
C++で堅牢なプログラムを作成するには、エラーハンドリングが不可欠です。ファイルが見つからない、ネットワークに接続できない、不正な入力が行われた、といったエラーは日常的に発生します。これらのエラーを放置すると、プログラムはクラッシュしたり、データを破壊したりする可能性があります。
C++には、エラーを処理するためのいくつかの伝統的なアプローチがあります。この記事では、3つの主要なエラーハンドリング手法について、それぞれの長所と短所を解説します。
- 戻り値によるエラーハンドリング
- 例外によるエラーハンドリング (C++の標準的な方法)
- グローバル変数によるエラーハンドリング
1. 戻り値によるエラーハンドリング
関数が処理に成功したか失敗したかを示す特別な値(エラーコード、例: -1
や false
)を、関数の戻り値として返す方法です。C言語で広く使われてきた伝統的な手法です。
サンプルコード
#include <iostream>
#include <string>
// 文字列を整数に変換する。失敗した場合は-1を返す。
int convert_to_int(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
return -1; // エラーコードとして-1を返す
}
}
int main() {
int result = convert_to_int("123");
if (result != -1) {
std::cout << "変換成功: " << result << std::endl;
} else {
std::cout << "変換失敗" << std::endl;
}
result = convert_to_int("abc"); // 失敗するケース
if (result == -1) {
std::cout << "変換失敗" << std::endl;
}
return 0;
}
問題点:
- エラーと正常値の区別: もし
-1
が正常な値としてあり得る場合(例: 温度の測定)、エラーと正常値を区別できなくなります。 - コードの冗長化: 関数を呼び出すたびに、
if
文で戻り値をチェックする必要があり、コードが冗長で読みにくくなります。
2. 例外によるエラーハンドリング (推奨)
エラーが発生した時点で、throw
を使って「例外オブジェクト」を送出し、関数の呼び出し元にエラー処理を委ねる方法です。現代のC++における標準的なエラーハンドリング手法です。
サンプルコード
#include <iostream>
#include <stdexcept> // runtime_error
#include <string>
// 例外を投げる関数
void process_data() {
// ... 何らかの処理 ...
throw std::runtime_error("データ処理中にエラーが発生しました。");
}
void sub_task() {
process_data();
}
int main() {
// 1. tryブロックで、例外が発生する可能性のある処理を囲む
try {
sub_task();
}
// 2. catchブロックで、特定の型の例外を捕捉する
catch (const std::runtime_error& e) {
std::cerr << "エラーを捕捉しました: " << e.what() << std::endl;
}
return 0;
}
メリット:
- エラー処理の分離: 正常系の処理と、異常系の処理(
catch
ブロック)を明確に分離できるため、コードが非常にクリーンになります。 - 詳細なエラー情報:
e.what()
のように、エラーに関する詳細な情報を簡単に伝達できます。 - 呼び出し階層のスキップ: エラーが発生すると、
catch
ブロックが見つかるまで、関数の呼び出し階層を一気に遡ります。途中の関数でエラーチェックを繰り返す必要がありません。
3. グローバル変数によるエラーハンドリング
エラー情報を、プログラム全体からアクセスできるグローバル変数に格納する方法です。古いC言語のライブラリ(例: errno
)などで見られます。
サンプルコード
#include <iostream>
// グローバルなエラーコード変数
int g_error_code = 0;
void do_something() {
// 何らかの理由で失敗したと仮定
g_error_code = 1; // エラーコードを設定
}
int main() {
g_error_code = 0; // 実行前にリセット
do_something();
if (g_error_code != 0) {
std::cout << "エラーが発生しました。コード: " << g_error_code << std::endl;
}
return 0;
}
問題点: グローバル変数は、プログラムのどこからでも変更できてしまうため、大規模なプログラム、特にマルチスレッド環境では、いつ、どこでエラーが設定されたのかを追跡するのが非常に困難になり、バグの温床となります。この方法は、現代のC++では一般的に非推奨です。
まとめ
今回は、C++の3つの主要なエラーハンドリング手法を解説しました。
方法 | メリット | デメリット | 推奨度 |
戻り値 | シンプル | コードが冗長、エラーと正常値の区別が曖昧 | △ (限定的) |
例外 | エラー処理を分離でき、コードが明確 | try-catch 構文の理解が必要 | ◎ (推奨) |
グローバル変数 | (ほぼ無し) | バグの温床になりやすい | ☓ (非推奨) |
結論として、C++では、回復可能なエラーを扱う際には std::expected
(C++23) や std::optional
(C++17) を、回復不能な、あるいはプログラムの前提を崩すようなエラーを扱う際には「例外」を使うのが、最も安全でモダンなベストプラク-ティスです。