エラーハンドリングのまとめ＋個人メモ

#tl:dr
[IPAのエラーハンドリングの解説](https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/c502.html)の内容が少々不足していると思っていて、
少し考えたことを整理してみたくなったので公開する。できるだけ実践的な内容にしたい。C++ベースで説明。後半は個人のメモ。適宜更新予定。

# 用語の定義
「エラー」とは、ファイルハンドラーオープンの失敗、メモリやリソース確保の失敗、不正なメモリアクセス、論理的計算の間違えなど。詳細は後述。

エラー発生の系は以下に分類される。

|名称|説明|
|---|---|
|正常系|目的の出力や副作用を得ることができる系|
|異常系※|目的の出力や副作用を得ることができない系|
|准正常系|異常系であるが、仕様どおりに正常系への回復動作が実行される、あるいは、仕様どおりにプロセスが終了する系|
|名称なし※2|異常系であり、エラー発生以後の動作が不定になる系|
※エラー発生を未然に防ぐが目的を達しない系もあり、エラー発生がある場合を異常系というよりは、目的を達しない系を異常系と表現のほうが、正常系テスト、異常系テストと表現とも合致し、より適切と考える
※2ここでは、便宜的に制御不能系と表現する。偶然、正常系に復帰できる場合もあるが、実行環境を変えたり、再コンパイルした場合に、復帰できる保証はない。


「系」とは一連の動作、プログラムでいえば条件分岐を含む一連のステップ（計算や関数の呼び出し）。例えば、以下の場合

```c++

std::ifsream ifs(filename);//A 
char* buf = new char[100];//B
if(ifs.good()){
  //Doing somthing //C
}else{
  //Doing somthing //D
  delete [] buf; //E 
  return false; //F
}
//Doing somthing //G

buf[2000] = 10; //H
````

A->B->C->Gが正常系
A->B->D->E->Fが異常系であり准正常系
H以降が異常系（制御不能系）
となる。

エラー発生の状態の定義は、統一的なものがないが、ここでは、ステップの実行状態を以下のように定義する。アルファベットは上記の例のアルファベットに対応。

||実行中の系|エラー発生|状態変数、リソースリーク|動作の制御|対応する系|
|---|---|---|---|---|---|
|1|正常|なし|正常、なし|可能|C|
|2|准正常|あり|異常、ありうる|可能|D|
|3|准正常|あり|正常、なし|可能|F|
|4|制御不能|あり|不定|不能|H|


「エラーハンドリング」とは、エラー発生に対し准正常系を実装すること。あるいは、エラーが発生しても動作が継続可能で**恒常的**に正常な状態に復帰させるか、仕様どおりにプロセスを終了させることともいえる。

検知、伝達、対処から構成される。
![エラーハンドリングpng.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321607/a08ae11a-1d5c-fd08-9d03-9ba87ce96270.png)
四角は関数と考えてよい。関数が階層的に呼び出される。

「検知」とは、発生しうるエラーを事前に検知すること。発生したエラーを検知すること（＝伝達されたエラーを受け取ること）。
「伝達」とは、エラーが発生したことを関数や計算式の呼び出しもとに通知すること。
「対処」とは、動作の継続が可能であれば継続可能な状態にすること（正常系に復帰させること）。動作の継続が不能なら適切にプロセスを終了すること。プロセスを終了して再始動することも含む。

→例えば関数コールによってエラーを検知し伝達するときは、状態２と状態３のどちらかであるは把握して対処すべき。
リスースの所有権が特定の関数にあるときは、関数を抜けるときに解放すべき。リソースが関数間で共有される場合は、どの階層でリソースを解放するかを把握もしくは設計する必要がある。伝達中のどこかの関数で状態２から状態３にする。
最終的な対処を実施する関数まで戻り対処したのち、正常系の動作に移る。つまり、状態を１にする。ソフトウェアが状態４になってはいけない。

# エラー検知方法
0.エラーが発生する前に、エラーが発生する可能性があるか判定する。
これを検知と呼ぶかは議論の余地もあるが、一般的にエラーハンドリングの一手である。

```c++
//検知しない例
int a = b/c;   

//事前に検知し対処（回避）するのでゼロ割というエラーは発生しない
int a = c != 0 ? b/c : 0;   
```

1. 例外をキャッチ
2. 関数コール時に、戻り値で受け取る
3. メンバー変数（関数の戻り値）を判定する
4. グローバルな変数（関数）で判定する
5. ライブラリー、フレームワークが提供する関数をコールし戻り値を判定する
6. ライブラリー、フレームワークが提供するハンドラーを使う


# エラー伝達方法
1or2が一般的。
1. 例外をthrow
2. 関数の戻り値でエラー状態を返す
3. メンバ変数を設定
4. グローバルな変数を設定
5. ライブラリー、フレームワークが提供する関数を使って状態を設定
6. ライブラリー、フレームワークが提供する関数をコール
　
# エラー対処方法
## プロセスを終了させない方法
- スキップ（中断）
例）ファイルオープンに失敗したので、以降の読み取り処理も中断し、呼び出しもとにエラーを返す。呼び出しもとは、ファイルがオープできなかったことを前提に准正常系の処理を実行する。

- デフォルト値を返す
- 値のクリップ

```c++
int i = std::rand();  
auto b = static_cast<std::byte>(std::clamp(i,0,255));   
```
- 一定時間、または、一定回数分リトライ
- 代替え手段を実施
- ユーザーにデータの再入力を促す
- ユーザーに通知（ログに残す、メッセージダイアログを表示）
  「スキップ」や「ユーザーにデータの再入力を促す」などと併用

## プロセスを終了させる方法
- ユーザーへのメッセージやログを出力して終了
- プロセスを再起動
例)Google Chromeのタブ（一つのタブがプロセス）
- マシンとプロセスを再起動

# エラーの種類
## 1.論理エラー
プログラムの論理的矛盾により発生。
→開発段階でエラーが発生しないよう設計にすべき。

### 静的要因により発生
- 計算式の誤りなど
- 配列インデックスの範囲外アクセス

```c++:例
int x = 1/0;
int a[10]; a[20]=1;
```

## 2.実行時エラー
実行時の状態により発生。
→エラーは発生を事前に抑止できないので、エラーハンドリングコードを実装する必要ある。
### 入力データに依存して発生
- 想定外の入力データを使ったことにより計算

```
std::ifstream ifs("data.bin"); 
char buf[255];
ifs.read(buf,255);
1/buf[0]; buf[0]=0でエラー発生
```
### 実行時のOSやPCの状態によって起こりうるエラー
- ヒープメモリの確保の失敗
- リソース確保の失敗
- メモリ確保した範囲外を参照　
- ファイルオープンの失敗
- ファイル書き込みの失敗（空きディスク不足など）
- ウィルス・放射線などの外的要因でメモリ上のデータが破壊される
- 文字列のパースに失敗
- 通信処理で、通信経路、通信先のトラブルによる　

# シーケンシャルな処理のエラーハンドリング例
エラーハンドリングなしのフロー

![flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321607/b9514ae1-679b-2be7-79ef-8887eda064ef.png)



これにエラーハンドリングを実装するなら以下のようにする。





![flow2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321607/e183a5d0-2c7e-921d-2be1-797d33258bbb.png)

<!-- 
```flow
st=>operation: イニシャライズ/データ入力
main=>subroutine: メイン処理
out1=>operation: データ出力
e=>operation: ファイナライズ

st->main->out1->e
```

```flow
st=>operation: イニシャライズ/データ入力
main=>subroutine: メイン処理
out=>operation: データ出力
e=>operation: ファイナライズ

stc=>condition: イニシャライズ/データ入力成功?
mainc=>condition: メイン処理成功(例外発生なし)？
outc=>condition: データ出力成功?
e=>operation: ファイナライズ

st->stc
stc(yes)->main
main->mainc
stc(no)->e
mainc(yes)->out
mainc(no)->e
out->outc
outc(yes)->e
outc(no)->e
```
-->
メイン処理にはtry～catchを設定してもよい。例外を使うことを推奨しない人もいるが、例外を使うライブラリが多数存在する以上、ライブラリ設計者の意図を組んで、素直に例外をハンドリングしたほうが実装が簡潔（条件分岐が減ることに）になるなら、作法にしたがうべき。例えば、std::vevctorにpush_backするとき、我々が目にするサンプルソースではtry～catchが設定されないが、一定の品質を確保する必要があるソフトウェアでは、本来設定すべい。メイン処理の最後で例外をcatchしたら、ファイナライズ処理に飛ぶ。逆に、リソース確保が多くエラーが発生しやすいいイニシャライズ部内は、関数の戻り値をチェックしたほうがエラーハンドリングという観点でいえば、利点が多い。例えば、検知と対処がコード上で対になり因果関係が明確。例外の場合、二箇所で同じ例外をthrowする場合、エラーの発生箇所の特定に特別な処理が必要。また、関数コールと対処コードが近いので、対処コードの中で関数の引数を参照しやすく、より具体的なエラーメッセージを作ることができる。イニシャライズ部で確保したリソースはファイナライズで解放するこが前提であるが、C++の場合は、RAIIを積極的に利用すべきで、そのときはファイナライズ部に解放コードは不要で、そのまま終了（サブルーチンならリターン）できる。

# 非同期処理のエラーハンドリング
非同期処理はそのモデルやライブラリが多いので、ライブラリごとに用意された機構を使うべき。ただ、例外は同期処理単位（スレッド）で補足し、ライブラリが提供する手段でエラーを通知する。

#　参考
https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/c502.html


#　その他 
- ソフトウェアのタイプに応じて、どこまで異常を検知すべきか決めるべき（開発コストをかけるべきか）<br>
-- OS/Driver<br>
-- 長時間稼働のアプリケーション<br>
-- 短期間稼働のアプリケーション<br>
-- ライブラリ<br>
-- 個人で使うツール<br>
- メモリーリーク、リソースリークは徹底的になくす<br>
　intel inspector, valgrindを使う
- 他人にリリースするコードは、メモリ確保、リソース確保の失敗を徹底的に検知する
- 処理の中断は、ユーザーにとってどんな影響があるか考える
　時間のかかる処理の場合、早い段階でエラーを検知し中断する。遅い段階の中断では、待ったユーザーに腹を立たせる
- ソフトウェア内部で完結するエラー対処ができない場合、エラーログを出力したほうがよい<br>
　C++のオススメのロギングライブラリ<br>
　https://github.com/gabime/spdlog
- stlコンテナのresize,reserve,push_xxxは、実質メモリ確保を伴うのでメモリ確保と同等のエラー検出を行うべき 
- 例外クラスを作るときは、Opencvの例外が参考になる。<br>
https://docs.opencv.org/4.1.0/d1/dee/classcv_1_1Exception.html#details
例外オブジェクトに、ファイル名、関数名、行番号を設定でき、catch側で情報を受け取れるのがポイント。<br>
ファイル名や行番号は\__FILE\__や\__LINE\__マクロで取得できる、詳細はは以下を参照。<br>
https://ja.cppreference.com/w/cpp/preprocessor/replace<br>
c++11では、現在実行中の関数名もマクロで取得できる。<br>
https://cpprefjp.github.io/lang/cpp11/func.html<br>
- 例外をcatchしたとき
例外には、以降の実行が不可でプロセスを終了させべきものもあるが、実行を継続できるものもあるので、例外発生の原因を理解した上でソフトウェア実装したほうがよい。
- 例外の参考資料<br>
http://www.02.246.ne.jp/~torutk/cxx/exception/programming.html

#　履歴
2019/6/4 初版
2019/6/5 フローが表示されない問題の訂正、誤字脱字の訂正、若干表現を変えた