例外とエラーをまぜまぜできるライブラリ xreturn を作ってみた。
これは C++11 Advent Calendar 2011 の記事です。
ご意見ご感想は、こちらまでお気軽に・・・よりよいコードのために議論したいです。
なお、少し前に、エラー処理を書いてはいけないという記事がでてしまって、非常に困った感じなのですが、エラーハンドリングのはなしをしますwwww
だって、これしかネタないんですよ。。。
仕方ないよね。仕方ないですよね。これはもうやるしかないですよね。
例外とエラーの話しはおいといて
例外とエラーは違うし、
そもそも何を持って例外で、何を持ってエラーかというのは、使う場合によるというのが答えになるでしょう。
(詳細を書くときりがないので、次回の勉強会とかで議論しましょう????)
で、
例外とエラーをまぜまぜしてシンプルに書けるよ。
ちょっと前に例外とエラーをまぜこぜにしてシンプルに記述できるライブラリを作りました。
で、今回は、そいつをいろいろ変更しまくってパワーアップさせてみました。
ソースはgithubにあるよ。
VC++10 と gcc 4.6.1で動作確認をしています。C++11 が利用できれば大丈夫です。
こんな風に書けます。
//fizzbuzz の変化形に出会うとエラーになります。 //それ以外は入力した値が帰ります。 xreturn::r<int> fizzbuzz(int a) { if (((a % 3) == 0) && ((a % 5) == 0)) { return xreturn::error("3と5で割り切れますがな",35); //エラーにしたい } if ((a % 3) == 0) { return xreturn::error("3で割り切れますがな",3); //エラーにしたい } if ((a % 5) == 0) { return xreturn::error("5で割り切れますがな",5); //エラーにしたい } return a; }
使う側はいろいろな使い方ができます。
既存の書き方に合わせるとこんな感じです。
ふつーでしょ?
int a1 = fizzbuzz(1); std::cout << a1 << std::endl; //1が帰る
もし、エラーが発生する組み合わせを呼んで、それで何かしようとすると、キャストした瞬間に例外が飛んでくれます。
int a3 = fizzbuzz(3); //例外 xreturn::error("3で割り切れますがな",3); が飛ぶ。
エラーを無視されたとしても同様です。(前回からのパワーアップ)
printf や fseekみたいに呼ぶことに意義がある関数でも効率的に書けますね。
fizzbuzz(1); //何も起きない fizzbuzz(3); //例外 xreturn::error("3で割り切れますがな",3); が飛ぶ。
いやいや、勝手に例外を飛ばしてくれたら困るよ。
僕はエラーハンドリングをきっちりしたいんだよって人の需要も満たします。
auto r1 = fizzbuzz(1); //1なので正しい処理結果が帰ります。 if (r1.isOK()) { //通常の処理をココに書きましょう。 } auto r3 = fizzbuzz(3); //3で割り切れるのでエラーになります。 if (r3.isError()) { //エラー処理を書けます。 }
autoでいちいち受けるとかダサくない?って人も大丈夫です。
if (!fizzbuzz(3)) { //エラー処理を書けます。 }
エラーコードで分岐させたいんだけどって人も大丈夫。
auto r3 = fizzbuzz(3); if (!r3) { if ( r3.getErrorCode() == 3 ) { } else if ( r3.getErrorCode() == 3 ) { } else if ( r3.getErrorCode() == 35 ) { } }
エラーコードとかダサくない?型でエラーを表現するのが今のトレンドなんだよっって人もご安心を。
(前回からのパワーアップ)
class error_fizz : public xreturn::error { public: error_fizz(){} error_fizz(const std::string & message , int code) : xreturn::error(message,code){} virtual ~error_fizz(){} }; class error_buzz : public xreturn::error { public: error_buzz(){} error_buzz(const std::string & message , int code) : xreturn::error(message,code){} virtual ~error_buzz(){} }; class error_fizzbuzz : public xreturn::error { public: error_fizzbuzz(){} error_fizzbuzz(const std::string & message , int code) : xreturn::error(message,code){} virtual ~error_fizzbuzz(){} }; xreturn::r<int,error_fizz,error_buzz,error_fizzbuzz> fizzbuzz_for_type(int a) { if (((a % 3) == 0) && ((a % 5) == 0)) { return error_fizzbuzz("3と5で割り切れますがな",35); } if ((a % 3) == 0) { return error_fizz("3で割り切れますがな",3); } if ((a % 5) == 0) { return error_buzz("5で割り切れますがな",5); } return a; } //使う側は、こんなふうに書けます。 //C++のswitch文がもうちょっと賢くなるともっといい感じにできるかもしれないね。 auto z3 = fizzbuzz_for_type(3); if (z3.type() == typeid(int) ) //if (!z3) や z3.isOK() と同じ意味です。 { std::cout << z3 << std::endl; //3が帰るが、今回はエラーなのでココには来ない MY_ASSERT(0); } else if (z3.type() == typeid(error_fizz) ) { std::cout << "fizz" << std::endl; } else if (z3.type() == typeid(error_buzz) ) { std::cout << "buzz" << std::endl; MY_ASSERT(0); //今回は3 fizzなのでここにはこない } else if (z3.type() == typeid(error_fizzbuzz) ) { std::cout << "fizzbuzz" << std::endl; MY_ASSERT(0); //今回は3 fizzなのでここにはこない }
とりあえず、こんな感じです。
通常の正常系の処理の流れを邪魔せずに、かつエラー処理を柔軟に書けます。
関数を呼び出す側で制御できるので、「何を持って例外か?それは場合による。」といった問題に対応できます。
使い方は?
もちろん、ヘッダーファイル xreturn.h を include するだけで利用できます。
簡単ですね。
でもコストがかかるんでしょ?
10,000,000回呼び出して、5倍ぐらいの差がでます。
5倍といっても、109ミリ秒程度ですが・・・。
これは、エラーを握りつぶしていないかを確認するフラグ管理に支払っているコストだと思われます。
ベンチマーク 10000000回呼び出します 非xreturn: 31ms xreturn: 140ms
実装が見たいとな・・・
実装はこんな感じです。
長いですね。githubを見たほうがいいんじゃなイカ。
#pragma once #include <string> #include <typeinfo> // //戻り値にエラーを絡ませられるようにしてみたライブラリ // // namespace xreturn { //エラーコードと std::string もいけるようにした例外クラス //xreturn::error として使ってね。 class error : public std::exception { protected: std::string message; // int code; //std::exceptionがなぜか保持してくれないので自分で保持する. public: error() { } error(const std::string & message , int code) : std::exception() { this->message = message; this->code = code; } virtual ~error() throw() { } virtual int getErrorCode() const { return this->code; } virtual std::string getErrorMessage() const { return this->message; } virtual const char* what() const throw() { return this->message.c_str(); } }; //テンプレートの型で関数作りたいときに同じ型だと衝突して死ぬので回避する. class dummy_error2 : public xreturn::error{}; class dummy_error3 : public xreturn::error{}; class dummy_error4 : public xreturn::error{}; class dummy_error5 : public xreturn::error{}; class dummy_error6 : public xreturn::error{}; class dummy_error7 : public xreturn::error{}; class dummy_error8 : public xreturn::error{}; //例外から派生している型を格納する class any_error_base { public: virtual void throwException() const = 0; virtual const xreturn::error& get() const = 0; virtual const std::type_info& type() const = 0; virtual ~any_error_base(){} }; template<typename ERRORTYPE> class any_error : public any_error_base { ERRORTYPE error; public: any_error(const ERRORTYPE & e) : any_error_base() { this->error = e; } virtual ~any_error() { } virtual void throwException() const { throw this->error; } virtual const xreturn::error& get() const { return this->error; } virtual const std::type_info& type() const { return typeid(ERRORTYPE); } const ERRORTYPE& getError() const { return &this->error; } }; //エラーをラップする構造 template <typename TYPE ,typename ERRORTYPE1 = xreturn::error ,typename ERRORTYPE2 = dummy_error2 ,typename ERRORTYPE3 = dummy_error3 ,typename ERRORTYPE4 = dummy_error4 ,typename ERRORTYPE5 = dummy_error5 ,typename ERRORTYPE6 = dummy_error6 ,typename ERRORTYPE7 = dummy_error7 ,typename ERRORTYPE8 = dummy_error8 > class r { private: TYPE value; any_error_base* error; mutable bool nigirichubushi; //すまぬ・・・ const auto に対応するために、 mutableにさせてくれ。 public: r() { this->error = nullptr; this->nigirichubushi = false; } virtual ~r() { if (this->nigirichubushi) { //非常に悩ましいが、あえてデストラクタで例外を投げる。 //エラーをチェックしないで握りつぶすようなコードで例外処理を行うにはこれしか方法がない。。。 if ( this->isError() ) { //deleteで消し終わってから、throw exception したいんだけど、 投げる例外が消したいメモリ空間にあるわけで・・・ //これはあもりにもジャンクコードすぎるのでなんとかしたいんだけど。 if ( typeid(ERRORTYPE1) == this->error->type() ) { auto e = *(dynamic_cast< any_error<ERRORTYPE1>* >(this->error)); delete this->error; this->error = nullptr; e.throwException(); } if ( typeid(ERRORTYPE2) == this->error->type() ) { auto e = *(dynamic_cast< any_error<ERRORTYPE2>* >(this->error)); delete this->error; this->error = nullptr; e.throwException(); } if ( typeid(ERRORTYPE3) == this->error->type() ) { auto e = *(dynamic_cast< any_error<ERRORTYPE3>* >(this->error)); delete this->error; this->error = nullptr; e.throwException(); } if ( typeid(ERRORTYPE4) == this->error->type() ) { auto e = *(dynamic_cast< any_error<ERRORTYPE4>* >(this->error)); delete this->error; this->error = nullptr; e.throwException(); } if ( typeid(ERRORTYPE5) == this->error->type() ) { auto e = *(dynamic_cast< any_error<ERRORTYPE5>* >(this->error)); delete this->error; this->error = nullptr; e.throwException(); } if ( typeid(ERRORTYPE6) == this->error->type() ) { auto e = *(dynamic_cast< any_error<ERRORTYPE6>* >(this->error)); delete this->error; this->error = nullptr; e.throwException(); } if ( typeid(ERRORTYPE7) == this->error->type() ) { auto e = *(dynamic_cast< any_error<ERRORTYPE7>* >(this->error)); delete this->error; this->error = nullptr; e.throwException(); } if ( typeid(ERRORTYPE8) == this->error->type() ) { auto e = *(dynamic_cast< any_error<ERRORTYPE8>* >(this->error)); delete this->error; this->error = nullptr; e.throwException(); } } } delete this->error; this->error = nullptr; } //boolにキャストする場合 //これで実装すると、 xreturn::r<bool> の時に衝突するから operator で逃げる. // operator bool() // { // this->nigirichubushi = false; // return this->isOK(); // } bool operator !() const { return this->isError(); } bool operator ==(bool t) const { if ( typeid(TYPE) == typeid(bool) ) {//xreturn::r<bool>の場合は判別不能なので普通にキャスト operator TYPE() に飛ばす. return (*this); } return (t == true && this->isOK()) || (t == false && this->isOK()); } operator TYPE() const { this->nigirichubushi = false; if ( this->isError() ) { this->error->throwException(); //ここで例外を投げる } return this->value; } r<TYPE>& operator =(const TYPE & a) { this->value = a; this->error = nullptr; this->nigirichubushi = false; a.nigirichubushi = false; return *this; } r(const TYPE& a) { this->value = a; this->error = nullptr; this->nigirichubushi = false; } r(const ERRORTYPE1& e) { this->error = new any_error<ERRORTYPE1>(e); this->nigirichubushi = true; } r(const ERRORTYPE2& e) { this->error = new any_error<ERRORTYPE2>(e); this->nigirichubushi = true; } r(const ERRORTYPE3& e) { this->error = new any_error<ERRORTYPE3>(e); this->nigirichubushi = true; } r(const ERRORTYPE4& e) { this->error = new any_error<ERRORTYPE4>(e); this->nigirichubushi = true; } r(const ERRORTYPE5& e) { this->error = new any_error<ERRORTYPE5>(e); this->nigirichubushi = true; } r(const ERRORTYPE6& e) { this->error = new any_error<ERRORTYPE6>(e); this->nigirichubushi = true; } r(const ERRORTYPE7& e) { this->error = new any_error<ERRORTYPE7>(e); this->nigirichubushi = true; } r(const ERRORTYPE8& e) { this->error = new any_error<ERRORTYPE8>(e); this->nigirichubushi = true; } bool isOK() const { this->nigirichubushi = false; //すまぬ・・・ const auto に対応するために、 mutableにさせてくれ。 return this->error == nullptr; } bool isError() const { this->nigirichubushi = false; return this->error != nullptr; } TYPE check() const { this->nigirichubushi = false; return (*this); } const any_error_base* getError() const { //ASSERT(this->error != nullptr); this->nigirichubushi = false; return this->error; } int getErrorCode() const { return this->getError()->get().getErrorCode(); } std::string getErrorMessage() const { return this->getError()->get().getErrorMessage(); } //例外を投げる. void throwException() const { this->nigirichubushi = false; this->error->throwException(); } //現状の型情報を取得する const std::type_info& type() const { //成功している場合は、保持している型をそのまま if ( this->isOK() ) { return typeid(TYPE); } //エラーの場合は、エラー型をいただく. return this->getError()->type(); } };
ちょっおまwww
何?実装がいけていないとな?
mutable をロック以外に使っているし、 あろうことかデストラクタで例外を投げてるなんて、それは禁忌の領域ではないか。
エラーチェックを無視された場合の対処が、これしか思いつかなかったんだよ。
fizzbuzz(1); //何も起きない fizzbuzz(3); //例外 xreturn::error("3で割り切れますがな",3); が飛ぶ。(これをやりたい)
このように戻り値を無視されてしまったときは、 operator void とかが呼ばれわけでもないし、これをフックする方法がデストラクタを見ること以外思いつかなかった。
だから、こーゆー実装になっている。
一応、メモリ管理や2重のthrow 対策などは気を使って入るが・・・・
急募:これをなんとかする方法を知っている人は、twitterとかで教えてください。
きさま、本当に注意しているんだろうな。
さて、注意して入るといったが、それでも 2重throw になってしまう場合もあるのです。
例えば、以下のようなコードです。
auto r3_1 = fizzbuzz(3); auto r3_2 = fizzbuzz(3); int a = r3_2; //ここで例外。スタック巻き戻されるので r3_1 も例外を呼ぶ。
一応、これに対処する方法としては、「エラーチェックはまとめてやるのではなくて、一つづつやれよ」という方法があります。
//一つづつやる int r3_1 = fizzbuzz(3); //ここで例外 int r3_2 = fizzbuzz(3);
//一つづつやる auto r3_1 = fizzbuzz(3); //ここで例外 if (!r3_1) { } int r3_2 = fizzbuzz(3); if (!r3_1) { }
//一つづつやる int r3_1 = fizzbuzz(3).check(); //ここで例外 if (!r3_1) { } int r3_2 = fizzbuzz(3).check(); if (!r3_1) { }
まーつまり、ふつーに組んでいる限りは、 まとめてエラーチェックしてダブルthrow で死ぬなんてことはない「はず」なんだけど・・・。
気持ちがいいものではないので、なんとかしたいね。
今は、これ以上の方法を思いつかないけども。
まとめ
もう一度 xretrun を使ったコードを見てみよう。
なんか、けっこういけてね?(自画自賛)
xreturn::r<int,error_fizz,error_buzz,error_fizzbuzz> fizzbuzz_for_type(int a) { if (((a % 3) == 0) && ((a % 5) == 0)) { return error_fizzbuzz("3と5で割り切れますがな",35); } if ((a % 3) == 0) { return error_fizz("3で割り切れますがな",3); } if ((a % 5) == 0) { return error_buzz("5で割り切れますがな",5); } return a; } //使う側は、こんなふうに書けます。 //C++のswitch文がもうちょっと賢くなるとパターンマッチみたいにできるかもしれないね。 auto z3 = fizzbuzz_for_type(3); if (z3.type() == typeid(int) ) //if (!z3) や z3.isOK() と同じ意味です。 { std::cout << z3 << std::endl; //3が帰るが、今回はエラーなのでココには来ない MY_ASSERT(0); } else if (z3.type() == typeid(error_fizz) ) { std::cout << "fizz" << std::endl; } else if (z3.type() == typeid(error_buzz) ) { std::cout << "buzz" << std::endl; MY_ASSERT(0); //今回は3 fizzなのでここにはこない } else if (z3.type() == typeid(error_fizzbuzz) ) { std::cout << "fizzbuzz" << std::endl; MY_ASSERT(0); //今回は3 fizzなのでここにはこない }
まー、 xretrun 使うと、エラーをシンプルに書けるんじゃないの?
実装は一部いけていないところはあるけども。
エラーのいいところ(ハンドリングが柔軟)、例外のいいところ(無視されない、広域離脱、付加情報)のいいとこ取りできるんじゃね?
使う側も、それほど負荷をかけずに使う人はできるし、ライブラリを作る側も、それほど難しくはないでしょう。
そして、何よりも、何を持って例外か?って議論に終止符を打てたらいいなー。
おしまい。