🤔

結局C++でexceptions-basedよりreturn-error-basedでコード書いているなという訳

2023/12/10に公開

はじめに

C++でコードを書いていてexceptions-basedよりreturn-error-basedでコードを書いているなと思ったので、その理由をメモ。皆様はどう思いますかね。

return-error-based

戻り値でエラーコードの情報を返すスタイル。授業でC言語から習った人が初めに見るのがこれ。

int h() {
    int ret = f();
    if (ret != OK) {
        return GENERAL_ERR;
    }
    
    //...
    
    return OK;
}

intだけではなくenumだったりboolだったりする。最近ではstd::optionalstd:: expectedが出てきたりもしている。

std::expected<int, std::string> idiv(int a, int b)
{
  if (b == 0) {
    return std::unexpected{"divide by zero"};
  }
  if (a % b != 0) {
    return std::unexpected{"out of domain"};        
  }
  return a / b;
}

void h()
{
  auto v = idiv(10, 2);
  if (v) {
    std::cout << *v << std::endl;
  } else {
    std::cout << std::quoted(v.error()) << std::endl;        
  }
}

exceptions-based

エラー処理に例外を使う場合の書き方はC++ではこんな感じ

void h() {
    try {
        auto num = toInt("hogehoge");
        std::cout << num << std::endl;
    } catch (const std::runtime_error& e) {
        std::cout << e.what() << std::endl;
    }
}

例外が発生する部分をtryで囲む。その区間で例外がthrowされるとcatchで補足されてエラー処理が行われる。

exceptions-basedのメリット

int f() {
    bool b = g();
    if (b != true) {
        return GENERAL_ERR;
    }
    
    //...
    
    return OK;
}
int h() {
    f();
    // fの戻り値の無視することは簡単
    
    //...
    
    return OK;
}

関数fからすると自身が作った処理でもない、関数gの中で発生したエラーを上位に伝える必要がありエラーの変換処理を都度かかなくてはいけない。また、関数hで行われているようにエラーも無視が簡単すぎる。そこで例外処理を使うとわかりやすいコードを書くことができる。

void f()
{
    //...
    g();
    //...
}

void foo()
{
    try {
        g();
    }
    catch (runtime_error& e) {
        error_message(e.what());
    }
}

エラーコードと異なり黙って見過ごされることがなく、自動的に呼び出し元に伝搬されていく、また、エラーコードではif分で制御フロー中に例外ハンドリングのコードが混ざるが、例外ではcatchの中に隔離することができる。あと、C++ではコンストラクタや演算子オーバーロードからの直接的なエラー通知手段としては優れている。
また、例外処理はエラーコードに比べて時間がかかるが、このデメリットも例外だからという理由より、そもそも頻繁に例外が発生する異常事態になっていること自体が問題だったりする。

それでもreturn-error-basedを使う理由

exceptions-basedのメリットを見ると、よほど高速なリアルタイム性が要求されるのでなければ採用すべきだろうが、結局、あまり採用しないことが多くて、理由を考えると、Cleaner, more elegant, and wrongCleaner, more elegant, and harder to recognizeで言われているように単純に扱いが難しいからだと思う。

int h() {
    f();
    g(s());
    
    return OK;
}

上のコードを見たときにreturn-error-based で書いているプロジェクトなら、
 「おいおい、戻り値とっていないじゃん。」
と直ぐ悪いコード気が付けるがexceptions-basedで書いていると
 「上でエラー処理まとめてやっているのかな?」
と直ぐ気が付くことができないです。更にいうと戻り値を無視しているかは検査ツールで用意に検査できるため、うっかり補足し忘れたということはほぼないでしょう。また、C++で適用している領域がまさしく高速なリアルタイム性を要求していることが多いので大部分のエラー処理はreturn-error-basedを採用することになるのかなと思います。

参考情報

Cleaner, more elegant, and wrong
Cleaner, more elegant, and harder to recognize
P0323R10 std::expected

Discussion