とくにあぶなくないRiSKのブログ

危ないRiSKのブログだったかもしれない。本当はRiSKだけどググラビリティとか取得できるIDの都合でsscriskも使ったり。

constexpr な関数・クラスでのエラーハンドリング

背景・動機

constexpr な関数・クラス(以降、クラスを省略して単に関数と表記します)は、その性質上コンパイル時(compile-time)と実行時(runtime)のそれぞれで用いられます。でも、その両方の場面を考慮したエラーハンドリングの方法がまだ確立されていない(よね?少なくとも日本語の情報は無い(よね?))のでその取っ掛りを作ろうかな、と。
constexpr な関数で発生するコンパイルエラーは非常に分かりにくいです。少しでも分かりやすいコンパイルエラーを出してもらえるようなエラーハンドリングを目指したいな,と。ただし、これは処理系に依存しちゃいます。今回は g++4.7 にてテストしました。

対象読者

C++er. C++0xを常用してる人。constexpr を愛する人。constexpr が憎い人。その他、興味を持ってしまった変態。

お願い・期待

なんらかのコメントあると嬉しいです。この文章を踏み台に発展させてくれる人いたらいいな。
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:なかなか分かりやすいコンパイルエラーを出せなくて…。