例えば次のような関数があるとする。
constexpr int multiply(int a, int b) { return a * b; } inline int func(int a, int b, int c) { return a + multiply(b, c); } int main() { int a = 1; std::cout << func(a, 2, 3) << std::endl; return 0; }
この中で、multiplyは定数式評価可能である。しかし定数式評価不可能なfuncからその引数b、cを用いて呼び出されている都合上、このままでは定数式評価できない(はず……)。ただユーザーの目線では、第2、第3引数はどちらも定数であり、コンパイル時に計算できて良いはずである。
このような実行時に呼び出される関数の中の一部を定数式として利用したい場合、どうすればよいのか。色々と試してみたが、私にはスマートな方法が思いつかず、あまり美しくない代替案に行き着いている。
私はconstexprについては勉強中なので、実はもっと簡単な方法があるのに見落としているだけかもしれない。目から鱗が落ちるような素晴らしい方法を知っている方はぜひ教えて欲しい。
失敗例
私は当初、次のようなconst_intクラスを作成し、値ではなく型情報として定数評価可能な引数を伝達しようとした。
template <int X> struct const_int { constexpr operator int() const { return value; } static constexpr int value = X; }; template <class Int1, class Int2> int func(int a, Int1 b, Int2 c) { return a + multiply(b, c); } int main() { int a = 1; //_const_intはconst_int型に変換するユーザー定義リテラル。 std::cout << func(a, 2_const_int, 3_const_int) << std::endl; return 0; }
しかし2つほど問題がある。まず、演算子オーバーロードは一般にstaticにできないので、funcの引数に与えられたb、cからのintへのキャストは定数式評価できない。operator int
をconstevalにしても駄目だった。次に、a + multiply(b, c)
という文全体はコンパイル時に評価できないので、multiplyも実行時に呼び出されてしまうらしい。このあたりの詳細なルールを私はまだ把握できていない。
これらの問題を解決する方法はあるのだろうか?
方法1 multiplyの分岐
constexpr int multiply(int a, int b) { return a * b; } template <int I1, int I2> consteval int multiply(const_int<I1> a, const_int<I2> b) { return multiply(I1, I2); } template <class Int1, class Int2> int func(int a, Int1 b, Int2 c) { return a + multiply(b, c); } int main() { int a = 1; //_const_intはconst_int型に変換するユーザー定義リテラル。 std::cout << func(a, 2_const_int, 3_const_int) << std::endl; return 0; } //func、main、int版のmultiplyはいずれも失敗例と同じもの。
新たにconstevalなconst_int版multiplyを追加で定義してしまうという何とも本末転倒な方法。とはいえ、multiplyを中継する関数を一個追加するだけなので、このような使い方をしたい関数の数が多くなければそれほど酷い記述コストではない。引数がintではなくconst_intの場合は一旦このmultiplyを経由してint版multiplyが呼ばれ、またconstevalであるためどのような文脈でも必ずコンパイル時に評価されるので、目的は達成できる。
方法2 funcの分岐
constexpr int multiply(int a, int b) { if (std::is_constant_evaluated()); else std::cout << "not constant evaluated" << std::endl; return a * b; } template <class T> struct is_const_int : std::false_type {}; template <int I> struct is_const_int<const_int<I>> : std::true_type {}; template <class T> inline constexpr bool is_const_int_v = is_const_int<T>::value; template <class Int1, class Int2> int func(int a, Int1 b, Int2 c) { if constexpr (is_const_int_v<Int1> && is_const_int_v<Int2>) { constexpr int d = multiply(Int1::value, Int2::value); return a + d; } else return a + multiply(b, c); } int main() { int a = 1; //_const_intはconst_int型に変換するユーザー定義リテラル。 std::cout << func(a, 2_const_int, 3_const_int) << std::endl; return 0; }
もはや当初の目的を忘れかけているのではと思えてくるほどに間抜けな方法。なんかもう普通にテンプレート引数で与える方が楽な気がする。仮にmultiplyのような関数が多数あり方法1を使うことが難しいとか、funcが実はもっと長たらしくてその中のごく一部を定数式評価したいとかなら、この方法は意味があるかもしれない。constexpr ifで分岐するかオーバーロードするかはお好きに。