constexpr if文を使う上での落とし穴:早期リターン
背景
constexpr if
文は条件付きコンパイル
// constexpr if文の説明のためのサンプルなので、std::is_same_vをそのまま使わないことはご了承ください
#include <type_traits>
template <class T>
constexpr auto f() {
if constexpr (std::is_same_v<T, int>) {
return true;
} else {
return false;
}
}
int main()
{
// Tがintなのでstd::is_same_v<T, int>がtrueとなり、trueが返ってくる
constexpr auto expected_true = f<int>();
// Tがfloatなのでstd::is_same_v<T, int>がfalseとなり、falseが返ってくる
constexpr auto expected_false = f<float>();
static_assert(expected_true);
static_assert(!expected_false);
}
である・・・と見せかけて、条件付き実体化抑制なので、
#include <type_traits>
template <class T>
constexpr void f() {
if constexpr (std::is_same_v<T, int>) {
// Tがintの時のみ発動してほしいが・・・?
static_assert(false);
}
}
int main()
{
f<float>();
}
ではstatic_assert(false)
が発動するのはよく知られた落とし穴だと思います。
この落とし穴については江添さんのブログ記事やcpprefjp - C++日本語リファレンスで書かれています。
ちなみに、上記の場合はstatic_assert(false)
をstatic_assert([] { return false; }())
にするなどの方法でT
がint
の時のみ発動するようになります。
今回は、これとは別のconstexpr if
文を使う上での落とし穴をご紹介します。
早期リターン
次のサンプルコードをご覧ください。
#include <optional>
template <class, class = void>
struct has_value_type : std::false_type {};
template <class T>
struct has_value_type<T, std::void_t<typename T::value_type>> : std::true_type {};
template <class T>
constexpr auto has_value_type_v = has_value_type<T>::value;
template <class T>
struct MyClassWithArithmeticValueType {
using value_type =T;
};
template <class T>
constexpr auto f() {
if constexpr (!has_value_type_v<T>) {
// T::value_typeがなければ0を返す
return 0;
} else {
// T::value_typeがなければ1を返す
// あくまで説明のためのサンプルなので、1をそのまま返さないことはご了承ください
return std::make_optional<typename T::value_type>(1).value();
}
}
int main() {
constexpr auto expected_zero = f<int>();
constexpr auto expected_one = f<MyClassWithArithmeticValueType<int>>();
static_assert(expected_zero == 0);
static_assert(expected_one == 1);
}
このサンプルでは、f<T>()
と呼んだ時、T
がvalue_type
を持つかどうかで分岐しています。
ここで、if
文の早期リターンを思い出してみましょう。
そうすると、関数f
において、else
は一見いらなそうに見えます。
そこで
template <class T>
constexpr auto f() {
if constexpr (!has_value_type_v<T>) {
// T::value_typeがなければ0を返す
return 0;
}
// T::value_typeがあれば1を返す・・・?
return std::make_optional<typename T::value_type>(1).value();
}
と書き換えると、残念ながら、コンパイルエラーになります。
何故でしょうか。
コンパイルエラーを見てみましょう(コンパイルエラーとなるコードのリンク)。
prog.cc:20:40: error: type 'int' cannot be used prior to '::' because it has no members
return std::make_optional<typename T::value_type>(1).value();
^
prog.cc:24:36: note: in instantiation of function template specialization 'f<int>' requested here
constexpr auto expected_zero = f<int>();
どうやら、T
がint
の場合も、std::make_optional<typename T::value_type>(1)
の実体化が抑制されていないようです。
そうです。else
句をなくしてしまったことで、constepxr if
文による条件付き実体化抑制の効力が(スコープ外なので)及ばなくなり、(int::value_type
が存在しないので)コンパイルエラーとなったのです。
ちなみに、今回の場合は、else
を使わずに
template <class T>
constexpr auto f() {
if constexpr (has_value_type_v<T>) {
return std::make_optional<typename T::value_type>(1).value();
}
return 0;
}
と条件を反転させてstatementの順番を逆にしてもコンパイルに成功します。
おわりに
constexpr if
の外側なんだからconstexpr if
の落とし穴ではなくね?というご意見もあろうかと存じます。そこで「constexpr if
を使う上での落とし穴」とさせて頂きましたので許してください。
今回のケースは、昔コンパイル時ニューラルネットワークフレームワークを書いていたときに、else
のあるなしやstatementの順番でコンパイルが通ったり通らなかったりして気づきました。
が、当時は適当にガチャガチャやって通してしまっていて、今回改めてちゃんと考えたので、記事にしてみました。
Discussion