🕳️

constexpr if文を使う上での落とし穴:早期リターン

2021/05/08に公開

背景

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; }())にするなどの方法でTintの時のみ発動するようになります。

今回は、これとは別の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>()と呼んだ時、Tvalue_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>();

どうやら、Tintの場合も、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