要約
結局、std::optionalのラッパーライクなクラスを自作しました。
#include"bdb/Optional.hpp"
bdb::Optional<int> maybe_fail(){return std::nullopt;};
bdb::Optional<> maybe_false(){return false;}
int main(){
auto hoge = maybe_fail().err_log("hoge");
auto fuga = maybe_false().err_log("fuga");
}
./bin/test.out
hoge @ err_log[src/test.cpp:14]
fuga @ err_log[src/test.cpp:15]
デバッグがしたかったんです
そもそもエラーには2種類あると考えています。
- バグ (起きてはいけない / コンパイル時やテスト時に取り除く)
- 例外 (起こりうる / 実行時にハンドリングする)
プログラミングの文脈でいうエラーハンドリングとは主に後者のことを指していて、C++にも「Cから伝わる戻り値を使った方法」や「例外機構を使った方法」があります。本稿のテーマであるstd::optionalもそのための機構でしょう。
一方で前者のデバッグに関しては、古き良きprintfデバッグやassertマクロ、あるいはデバッガーを使ったステップ実行なんかが使われるでしょうか。もちろんエラーハンドリング機構もデバッグのために使用できますが、正直私には面倒に思えていました。
私がやるような小規模個人開発においては、デバッグはエラーが発生した個所のログを表示して上に伝達できれば十分なのです。そのために一々関数の戻り値をチェックしてはprintfしたり、例外をキャッチしては出力して再スローしたり、大変億劫なのです。だからエラー処理をさぼって、どこからか例外が飛んできたけど発生個所がわからず、あたふたとデバッグする羽目になるのです。
もう少しシンプルにデバッグのためのログだけ出す方法はないのでしょうか。
//C++23に追加されるstacktraceライブラリならできるのだろうか?
monadic operationsが解決してくれるとおもってたけど
C++23からstd::optionalにmonadic operationsが追加されます。
これを使えば、optionalが無効値の時にお手軽にログが出せると思っていたのです。
関数の戻り値を全部optionalにしてしまえば、これはサイコーのデバッグ手法なのではと妄想していたのです。
std::optional<int> maybe_fail(){return std::nullopt;};
int main(){
auto hoge = maybe_fail().or_else([]{std::cout<<"error at "<<__FILE__<<__LINE__<<std::endl;});
//or
auto log = []{/*some log function*/};
auto hoge = maybe_fail().or_else(log);
}
しかしダメでした。
or_elseはvoidを返す関数は受け取れないのです。(提案初期にはあったような・・・)
だから実際はこうなります。
std::optional<int> maybe_fail(){return std::nullopt;};
int main(){
auto hoge = maybe_fail().or_else([]->std::optional<int>{
std::cout<<"error at "<<__FILE__<<__LINE__<<std::endl;});
return std::nullopt;
});
//and
//common log function is maybe impossible...
}
理想とは程遠いです。
なんか長いし、nulloptしか返さないのに一々戻り値の型を指定しないといけないのがツライです。
戻り値型をうまく推論する方法も思いつかないので、汎用ログ関数も作れませんでした。(少なくとも私には)
std::expectedならT=voidが許されるからいけるかとも思いましたが、提案文書を見る限りどうもダメそう・・・?
bool型も同じインターフェイスだと嬉しいです
関数はすべてoptionalで返しなさいというルールを作ったとき、じゃあbool型はどうするんだよというのが気になります。
戻り値をif文でチェックするならいいですけど、or_elseを使うなら大変困ります。
boolを使わずoptional<int>を使えばいいのかもしれませんが・・・
という訳で自作しました
ベースはstd::optionalのprivate 継承
標準ライブラリの継承はご法度なイメージがあるけど、private継承なら許されるはず?
methodをusingで引っ張り出します。
template<typename T>
class Optional : private std::optional<T>{
public:
using std::optional<T>::optional;
using std::optional<T>::operator*;
using std::optional<T>::operator->;
using std::optional<T>::operator bool;
//amd more
};
エラーログ関数を実装します
構造はor_elseと同じです。無効値の場合ログを書きます。
エラー個所の特定はC++20のsource_locationを使用しています。
Optional<T> err_log(std::string_view sv="",std::ostream& os = std::cerr,std::source_location sl = std::source_location::current()) const noexcept{
if(*this) return *this;
else{
os << sv<<" @ "<<"err_log["<<sl.file_name()<<":"<<sl.line()<<"]" <<std::endl;
return std::nullopt;
}
}
bool型を作ります
元の提案文書に選択肢としてあった、std::monostateの特殊化というアイディアを拝借しています。
数値->boolの暗黙変換を禁止したかったので、conceptで制約しています。
推論ガイドも忘れずに。
template<typename BOOL>
Optional(BOOL b) noexcept requires std::same_as<T,std::monostate> && std::same_as<BOOL,bool>{
if(b) *this = std::monostate{};
else *this = std::nullopt;
};
constexpr bool operator*() const& noexcept requires std::same_as<T,std::monostate>{
return has_value();
}
global関数など
比較演算(==,<=>)は自前実装です。
std::optionalへの変換関数を用意しておけばいけるかと思ったけどダメでした。private継承ってそうなのね。
あとはstreamへの<<も定義しています。
完成
晴れて、ログ書き出しメソッドのついたoptionalクラスができました。
今後、自作関数の戻り値はすべてOptionalにする予定です。
さらば例外!
今後の方針
std::vectorやstd::mapのat関数で、よく範囲外アクセスをしてしまうのです。
その時投げられた例外が、どこのどたなたから飛んできたのかわからない。
一つずつtyr-catchで囲むなんて、とてもやってられないです。
at関数の戻り値を本稿のOptionalに変更したクラスを作ろうかと思っています。
Jsonをよく扱います。
OSSを使用しているのですが、やはりat関数があり例外を投げてきます。
これもつらいので自作JSONライブラリを作ろうかと思います。
早期リターンがしたいです。
関数内部処理のどこかでnulloptになってしまったら、nulloptをreturnしたいのです。
でも毎回if文でチェックなんてやってられません。
C++26で??演算子が導入されることを期待します。