背景・動機
constexpr な関数・クラス(以降、クラスを省略して単に関数と表記します)は、その性質上コンパイル時(compile-time)と実行時(runtime)のそれぞれで用いられます。でも、その両方の場面を考慮したエラーハンドリングの方法がまだ確立されていない(よね?少なくとも日本語の情報は無い(よね?))のでその取っ掛りを作ろうかな、と。
constexpr な関数で発生するコンパイルエラーは非常に分かりにくいです。少しでも分かりやすいコンパイルエラーを出してもらえるようなエラーハンドリングを目指したいな,と。ただし、これは処理系に依存しちゃいます。今回は g++4.7 にてテストしました。
お願い・期待
なんらかのコメントあると嬉しいです。この文章を踏み台に発展させてくれる人いたらいいな。
constexpr な関数を書き始めた人が、この文章を読んだおかげでつまづかなかったらいいな。
constexpr でのエラーハンドリング
- 実行時
- エラーがあってもお茶を濁してそのまますすめる。
例えば、「とある関数で0除算が起きても、とりあえず0などの適当な値を返してしまう」など。
-
- 例外を投げる。
何かを throw する。constexpr な関数では基本的にreturnで得たい値を返す設計になるはずで、ほとんどエラーコードを返す余裕がない。ポインタや参照を使って値を変更させることもできないし。だったら throw 使うしかないよね、と。
利点:例外大好きっ子が喜ぶ。
問題点:例外大嫌いっ子が悲しむ。
-
- コンパイルエラーにする。
これはエラーが起きてから処理をするのではなく、通常の実行を不能にすることにより実行時のエラーを発生させないという前衛的なアプローチ。constexpr な関数をコンパイル時にのみ使えるようにする事を意味する。
利点:通常の実行の事を考える必要がなくなる。実行時に例外投げられるくらいなら、いっそ使えない方がいいわ!!!という人が喜ぶ。
問題点:constexpr な関数なのに "兼用" じゃなくなる。
- コンパイル時
- エラーがあってもお茶を濁してそのまますすめる。
利点:とりあえずコンパイル通しやすいかも。
問題点:おかしな計算の連鎖が起きる。問題を確認できるのが大抵実行時。
-
- どうにかこうにかしてコンパイルエラーにする。
利点:コンパイルすれば問題が起きたことにすぐ気付ける。実行時に問題を持ちこさない。
問題点:なかなかコンパイル通せないかも。どうやってコンパイルエラーにするの?(後述)
コンパイル時のエラーハンドリングと実行時のエラーハンドリングはそれぞれ別に組み合わせられるようにしたいよね。
どうやってコンパイルエラーにするの?
- 実行時
- 実装のない関数を呼出す。(正確にはリンクエラー)
- コンパイル時
- 非定数式を使う。constexpr ではない関数の呼出し、throw など。
- 実装のない関数を呼出す。
コード
というわけで、かなりの時間試行錯誤*1した結果、こんな小さなライブラリを使うといいんじゃないか、という結論になりました。ライブラリというよりイディオムに近いかな?
// constexpr な世界では、返却値の型に void を使えないので定義。 struct constexpr_void{}; // 実行時に使おうとするとエラーを起こせる。 constexpr_void do_not_use_in_runtime(); // never define // コンパイル時にconstexpr関数内の任意の場所でエラーを起こせる。また実行時に使おうとするとエラーを起こせる。 inline constexpr_void constexpr_error() { return do_not_use_in_runtime(); } // コンパイル時に expression が false ならエラーを起こせる。また実行時に使おうとするとエラーを起こせる。 inline constexpr constexpr_void constexpr_assert(bool expression) { return expression ? constexpr_void{} : constexpr_error(); } // コンパイル時に expression が false ならエラーを起こせる。また実行時に expression が false なら例外を投げる。 template<class Exception> inline constexpr constexpr_void constexpr_assert(bool expression, Exception const & exception) { return expression ? constexpr_void{} : throw exception; }
使い方・実践
お題。int const * をデリファレンスした値を返す constexpr 関数を書いてみます
まずはそのまま実装。エラーハンドリング無し。
constexpr int deref(int const * p) { return *p; } #include<iostream> int main() { static constexpr int a{}; { int runtime = deref(&a); std::cout << runtime << std::endl; constexpr int compile_time = deref(&a); std::cout << compile_time << std::endl; } }
実行結果:
0 0
お題に条件を追加。p が空ポインタであってはならない
ではエラーハンドリングしてみます。
- 実行時:例外を投げる
- コンパイル時:コンパイルエラー
constexpr int deref(int const * p) { return constexpr_assert(p, "except..."), *p; } #include<iostream> int main() { static constexpr int a{}; { int runtime = deref(&a); std::cout << runtime << std::endl; constexpr int compile_time = deref(&a); std::cout << compile_time << std::endl; } try { int runtime = deref(0); std::cout << runtime << std::endl; // constexpr int compile_time = deref(0); // std::cout << compile_time << std::endl; } catch(char const * e) { std::cout << e << std::endl; } }
実行結果:
0 0 except...
ちゃんと「実行時:例外を投げる」になってますね。
コメントを外すとコンパイルエラー:
a.cpp: In function 'int main()': a.cpp:46:39: in constexpr expansion of 'deref(0u)' a.cpp:28:40: in constexpr expansion of 'constexpr_assert [with Exception = char [10]]((p != 0u), (*"except..."))' a.cpp:23:47: error: expression '<throw-expression>' is not a constant-expression
ちゃんと「コンパイル時:コンパイルエラー」になってますね。p != 0u がまさにコンパイルエラーの原因を示しています。
エラーハンドリングの方法を変更
- 実行時:コンパイルエラー
- コンパイル時:コンパイルエラー
constexpr int deref(int const * p) { return constexpr_assert(p), *p; } #include<iostream> int main() { static constexpr int a{}; { int runtime = deref(&a); std::cout << runtime << std::endl; constexpr int compile_time = deref(&a); std::cout << compile_time << std::endl; } try { int runtime = deref(0); std::cout << runtime << std::endl; // constexpr int compile_time = deref(0); // std::cout << compile_time << std::endl; } catch(char const * e) { std::cout << e << std::endl; } }
実行結果(コンパイルエラー):
C:\Documents and Settings\RiSK\cckg7mlE.o:a.cpp:(.text$_Z15constexpr_errorv[constexpr_error()]+0x8): undefined refere nce to `do_not_use_in_runtime()' collect2.exe: error: ld returned 1 exit status
いちおう「実行時:コンパイルエラー」になってますね。正確にはリンクエラーですし、エラーメッセージが超分かりづらいのが難点ですが…。do_not_use_in_runtime() をキーワードになんとかしてください。
コメントを外すと(別の)コンパイルエラー:
a.cpp: In function 'int main()': a.cpp:46:39: in constexpr expansion of 'deref(0u)' a.cpp:28:27: in constexpr expansion of 'constexpr_assert((p != 0u))' a.cpp:16:57: error: call to non-constexpr function 'constexpr_void constexpr_error()'
ちゃんと「コンパイル時:コンパイルエラー」になってます。分かりやすいメッセージじゃないですか?
その他のケース
お茶を濁してそのまますすめる場合は、そういうコードを自由に書いてください。これは全くおすすめしないのでコード例も無しです。
CEL
今のところ CEL にも入れようかなーと考えてるけど、インターフェースがこれでいいか自信ないのでコメントほしいです。関数名をどうするかとか、条件は事前条件にするかエラーになる条件にするかなど。あとはヘッダ名か。"assert.hpp" とかでいいかなぁ…。
*1:なかなか分かりやすいコンパイルエラーを出せなくて…。