SFINAEでtemplate classのメンバ関数の実体化を制御する

プログラミングC/C++

問題設定

SFINAE1を用いて、template classのメンバ関数を実体化したりしなかったりしたい。例えば、typenameがvoidの時は実体化する関数を抑制したり、typenameに応じて関数を呼び分けてほしい場合が考えられる。

普通のSFINAEの感覚だと以下のように書いてみたくなるが、これだとコンパイルエラーになる。

template <typename T>
struct Hoge {
  // void用の関数
  auto func(void)
  -> std::enable_if_t<std::is_same<T, void>::value, void> {
    std::cout << "void" << std::endl;
  }

  // 非void用の関数
  auto func(void)
  -> std::enable_if_t<!std::is_same<T, void>::value, void> {
    std::cout << "not void" << std::endl;
  }
};

SFINAEはtemplateのsubstitution failureを無視してくれる機能なので、template classであっても非templateメンバ関数の実体化に失敗すると普通にエラーになる。

SFINAEはtemplateに関する機能なので、メンバ関数をtemplateメンバ関数化して回避しなければならない。回避の仕方は大きく分けて3通り。後に紹介する方法ほどおすすめの方法である。

本ページのサンプルコードは以下の場所にある。
member-sfinae-1.cpp

方法1|Dummyのtemplateを使う

よく目にするのはこの形式である。Uという変数(デフォルト値はT)を導入してsubstitution failureの形にする。

template <typename T>
struct Hoge {
  template <typename U=T>
  auto func(void)
  -> std::enable_if_t<std::is_same<U, void>::value, void> {
    std::cout << "void" << std::endl;
  }

  template <typename U=T>
  auto func(void)
  -> std::enable_if_t<!std::is_same<U, void>::value, void> {
    std::cout << "not void" << std::endl;
  }
};

std::is_same<U, void>::valueUによって値が変わる。したがって、メンバ関数funcの戻り値もUの値が決まるまで分からないので、SFINAEの対象になる。

この方法のメリットは可読性が高いこと。SFINAE特有の読みにくさはあるが、ぱっと見て分かりやすいコードになる。

一方で、この方法のデメリットは、利用者が誤用する余地が残ることである。

int main(int argc, char* argv[]) {
  Hoge<int> hoge;
  hoge.func<void>();  // void用の関数が呼ばれてしまう

  return 0;
}

Uのデフォルト値がTだが、呼び出し側はこのtemplate parameterを自由に設定することもできる。そのため、上記のように設計者が意図しない関数が呼ばれる可能性がある2

方法2|dummyのbool変数を持たせる

SFINAEで実体化抑制させるだけなら、template変数はtypenameでなくてもよい。例えば、必ずtrueになるbool変数を用いる方法が考えられる。

template <typename T>
struct Hoge {
  template <bool AlwaysTrue = true>
  auto func(void)
  -> std::enable_if_t<std::is_same<U, void>::value && AlwaysTrue, void> {
    std::cout << "void" << std::endl;
  }

  template <bool AlwaysTrue = true>
  auto func(void)
  -> std::enable_if_t<!std::is_same<U, void>::value && AlwaysTrue, void> {
    std::cout << "not void" << std::endl;
  }
};

std::is_same::value && AlwaysTrueAlwaysTrueの値が決まるまで戻り値の型が決まらないので、SFINAEによる実体化抑制ができる。

方法1と比較すると、bool変数を用いる方法は誤用される心配はない。もしAlwaysTruefalseがセットされた場合、実体化に失敗して呼び出せなくなるためである。

int main(int argc, char* argv[]) {
  Hoge<int> hoge;
  hoge.func<false>();  // Error! 呼び出し候補の関数がない

  return 0;
}

ただし、実体化できないとはいえ、template関数にfalseを代入できるように見えるのは少し気持ち悪い。もう少しスマートに解決したい。

方法3|template parameterにnullptr_tを用いる

c++14から、template parameterにstd::nullptr_tが使えるようになった3std::nullptr_tnullptrの一値しか取れない型だが、template parameterとして立派に機能する。

template <typename T>
struct Hoge {
  template <std::nullptr_t Dummy = nullptr>
  auto func(void)
  -> std::enable_if_t<std::is_same<T, void>::value && Dummy == nullptr, void> {
    std::cout << "void" << std::endl;
  }

  template <std::nullptr_t Dummy = nullptr>
  auto func(void)
  -> std::enable_if_t<!std::is_same<T, void>::value && Dummy == nullptr, void> {
    std::cout << "not void" << std::endl;
  }
};

理屈はbool変数を用いる方法と同様。std::nullptr_tは一値しかとらないとはいえ、std::is_same<T, void>::value && Dummy == nullptrDummyの値が決まるまで分からないので、SFINAEにより実体化抑制に使える。

std::nullptr_tを用いることでtemplate parameterの自由度を下げることができ、利用者に余計な心配をさせなくて済む。

notes

  1. SFINAE(Substitution Failure Is Not An Error)は、C++の黒魔術の一つ。template実体化時にうまく代入できなかった場合、コンパイルエラーとはならず単に無視してくれる機能である。
  2. 以下のようにすれば回避できるが、これをするぐらいなら後述するboolnullptrを用いる方法の方が可読性に優れると思う。

    template <typename T>
    struct Hoge {
      template <typename U = T>
      auto func(void)
      -> std::enable_if_t<std::is_same<U, void>::value && std::is_same<U, T>::value, void> {
        std::cout << "void" << std::endl;
      }
    
      template <typename U = T>
      auto func(void)
      -> std::enable_if_t<!std::is_same<U, void>::value && std::is_same<U, T>::value, void> {
        std::cout << "not void" << std::endl;
      }
    };

  3. 参考:https://cpprefjp.github.io/lang/cpp14/nontype_template_parameters_of_type_nullptr_t.html

プログラミングC/C++

Posted by komori