C++を用いたソフトウェア開発において、堅牢性を高めるための重要な要素の一つが例外処理です。
C++の標準ライブラリには、エラーの種類に応じて使い分けることができる例外クラスの階層構造が用意されています。
その中でも「std::logic_error」は、プログラムの論理的な誤りを示すために設計された特別な例外クラスです。
本記事では、std::logic_errorの役割や派生クラスの使い分け、さらには最新のC++26を見据えた例外設計のベストプラクティスについて詳しく解説します。
std::logic_errorの基本概念と役割
std::logic_errorは、ヘッダファイル<stdexcept>で定義されている標準例外クラスの一つです。
このクラスは、プログラムの実行前に理論上回避できるはずの論理的な誤りを報告するために使用されます。
具体的には、関数の引数が仕様を満たしていない場合や、オブジェクトの状態が操作に対して不適切である場合などが該当します。
C++の例外設計において、エラーは大きく分けて「論理エラー(Logic Error)」と「実行時エラー(Runtime Error)」の二つに分類されます。
論理エラーはコードの修正によって理論上はゼロにできるものであり、実行時エラーは外部リソースの不足などプログラム側では制御しきれないものを指します。
std::logic_errorを適切に投げることで、開発者はバグの早期発見やAPIの誤用防止に役立てることができます。
std::logic_errorとstd::runtime_errorの決定的な違い
例外設計において最も頻繁に議論されるのが、std::logic_errorとstd::runtime_errorのどちらを採用すべきかという点です。
std::logic_errorは、プログラミング上の不備を指摘するための例外です。
例えば、配列の範囲外アクセスや、nullポインタを許容しない関数へのnull渡しなどがこれに当たります。
一方で、std::runtime_errorはプログラムの外部要因に起因する問題を扱います。
ネットワーク接続の切断やファイルの読み込み失敗、メモリ不足などは、どれほど完璧にコードを書いても発生し得る問題です。
この二つの違いを理解するための指針を以下の表にまとめました。
| 特徴 | std::logic_error | std::runtime_error |
|---|---|---|
| 発生原因 | プログラマの不備(バグ) | 外部環境や入力データ |
| 回避の可能性 | コードの修正で回避可能 | 完全な回避は困難 |
| 主な用途 | 事前条件の違反チェック | リソース確保の失敗など |
| 対応策 | 呼び出し側のロジック修正 | 再試行やリカバリ処理 |
このように、std::logic_errorが投げられる状況は「修正すべきバグ」があることを意味しているという点が重要です。
std::logic_errorから派生する標準例外クラス
std::logic_errorは基底クラスであり、実際にはより具体的なエラー内容を示す派生クラスを利用することが一般的です。
主要な派生クラスとその利用シーンを詳しく見ていきましょう。
std::invalid_argument
関数の引数が不正な場合に投げられる例外です。
例えば、正の数しか受け付けない関数に負の数が渡された場合に利用します。
ただし、単なる範囲外であれば後述するstd::out_of_rangeの方が適切な場合もあります。
std::domain_error
数学的な「定義域エラー」が発生した際に使用されます。
例えば、平方根を計算する関数に負の数を与えた場合などが該当します。
主に数値計算を伴うライブラリで頻繁に利用されます。
std::length_error
オブジェクトが保持できる最大サイズを超えようとした場合に投げられます。
std::vectorなどのコンテナが、システムの制限を超えるメモリを確保しようとした際に見かけることが多いでしょう。
std::out_of_range
有効な範囲を超えたインデックス指定や、キーの検索失敗時に使用されます。
std::vector::at()やstd::map::at()などの標準メンバ関数でも採用されています。
境界チェックを行う際に最も利用頻度が高い例外と言えます。
実践的なコード例:std::out_of_rangeの活用
ここでは、カスタムクラスにおいてstd::logic_errorの派生クラスであるstd::out_of_rangeを使用する例を紹介します。
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
class DataRepository {
private:
std::vector<std::string> data;
public:
void add(const std::string& item) {
data.push_back(item);
}
// 指定したインデックスのデータを取得する関数
const std::string& get_at(size_t index) const {
if (index >= data.size()) {
// 論理エラーとして範囲外アクセスを通知
throw std::out_of_range("DataRepository: Index is out of range. Index: " + std::to_string(index));
}
return data[index];
}
};
int main() {
DataRepository repo;
repo.add("C++23");
repo.add("C++26");
try {
// 意図的に範囲外のインデックスを指定
std::cout << repo.get_at(10) << std::endl;
} catch (const std::out_of_range& e) {
// 例外メッセージを出力
std::cerr << "Caught exception: " << e.what() << std::endl;
} catch (const std::logic_error& e) {
// logic_errorとしてもキャッチ可能
std::cerr << "Logic error: " << e.what() << std::endl;
}
return 0;
}
Caught exception: DataRepository: Index is out of range. Index: 10
このコードでは、呼び出し側が正しいインデックスを指定することを期待しており、それを満たさない場合に例外を投げています。
これにより、無効なメモリ領域へのアクセスという深刻な未定義動作を未然に防いでいます。
例外設計のベストプラクティス:いつ投げていつ捕まえるか
std::logic_errorを効果的に活用するためには、設計段階でのルール作りが欠かせません。
誤った例外の使い方は、パフォーマンスの低下やコードの複雑化を招く可能性があります。
事前条件の検証に利用する
関数の実行に必要な条件(事前条件)が満たされていない場合、std::logic_errorを投げるのは非常に良い習慣です。
これにより、関数の利用者がマニュアルを読み飛ばしていても、実行時に誤りに気づくことができます。
キャッチして握りつぶさない
std::logic_errorは「プログラムのバグ」を示すものです。
本番環境のコードでstd::logic_errorを頻繁にキャッチして握りつぶすべきではありません。
もしこの例外が発生した場合は、キャッチして処理を継続するのではなく、ログを出力してプログラムを安全に終了させるか、デバッグ中に修正を行うべきです。
assertとの使い分け
デバッグビルド時のみチェックを行いたい場合は、assertマクロを使用することも検討してください。
製品版(リリースビルド)でのパフォーマンスを最優先し、かつバグが混入しない自信がある内部関数ではassertが適しています。
一方、ライブラリとして提供する場合や、外部からの入力に依存する箇所ではstd::logic_errorを選択するのが一般的です。
例外安全性の3つのレベル
例外を投げる設計を行う際、併せて考慮すべきなのが「例外安全性(Exception Safety)」です。
関数が例外を投げた際、システムの状態がどうなるかを保証するための概念です。
- 基本保証(Basic Guarantee): 例外が発生しても、リソース漏洩が発生せず、オブジェクトが有効な状態(不変条件が維持された状態)であることを保証します。
- 強い保証(Strong Guarantee): 例外が発生した場合、関数呼び出し前の状態に完全にロールバックされることを保証します(コミット/ロールバック・セマンティクス)。
- 投げない保証(No-throw Guarantee): 関数が例外を絶対に投げないことを保証します。
noexceptキーワードを用いて明示します。
デストラクタやムーブコンストラクタなどは、常に「投げない保証」を提供すべきです。
現代のC++における新しいエラーハンドリング
2026年現在のモダンC++開発では、すべてのエラーを例外で処理する手法は見直されつつあります。
特にパフォーマンスが重視される領域では、例外のオーバーヘッドが問題視されることがあります。
std::optionalとstd::expected
C++17で導入されたstd::optionalや、C++23で導入されたstd::expectedは、例外を使わずにエラーを通知する優れた手段です。
「値が存在しない可能性がある」だけの場合はstd::optionalを使用します。
「エラーの詳細情報を返したいが、例外によるスタック展開は避けたい」という場合にはstd::expectedが最適です。
論理的な誤りではなく、予測可能な失敗(ファイルが見つからない等)には、これらの戻り値ベースのアプローチが推奨されます。
契約プログラミング(Contracts)への期待
C++26では「契約(Contracts)」の導入が進められており、事前条件や事後条件を言語レベルで記述できるようになる予定です。
これにより、std::logic_errorを明示的にthrowしなくても、コンパイラが自動的にチェックを挿入してくれる未来が近づいています。
まとめ
std::logic_errorは、プログラミング上の不備を検出し、コードの品質を担保するために欠かせないツールです。
std::runtime_errorとの違いを明確にし、適切な派生クラスを選択することで、メンテナンス性の高いコードを実現できます。
しかし、例外はあくまで「予期せぬ異常」のために使うべきであり、通常の制御フローの一部として多用しすぎないことも重要です。
特に、現代的なC++ではstd::expectedなどの代替手段も存在するため、要件に応じて最適な手法を選択するバランス感覚が求められます。
本記事で紹介したガイドラインを参考に、より安全で堅牢なC++プログラムの設計に取り組んでみてください。
まとめ
C++における例外処理、特にstd::logic_errorの活用は、単なるエラー通知以上の意味を持ちます。
それはプログラムの仕様を定義し、開発者間のコミュニケーションを円滑にするための契約でもあります。
論理エラーを正しく扱い、バグを早期に発見できる構造を構築することが、結果として開発コストの削減につながります。
最新の言語仕様や標準ライブラリの進化を追いながら、常に最適な例外設計を追求していきましょう。
