メインコンテンツへスキップ

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

·1734 文字·
技術解説 C++ SFINAE STL
komori-n
著者
komori-n
目次

問題設定
#

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|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が使えるようになった2std::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の自由度を下げることができ、利用者に余計な心配をさせなくて済む。


  1. SFINAE(Substitution Failure Is Not An Error)は、C++の黒魔術の一つ。template実体化時にうまく代入できなかった場合、コンパイルエラーとはならず単に無視してくれる機能である。 ↩︎

  2. 参考: https://cpprefjp.github.io/lang/cpp14/nontype_template_parameters_of_type_nullptr_t.html ↩︎

Related

libstdc++のstd::functionの実装を眺める
·3220 文字
技術解説 C++ STL
move-onlyな関数を扱えるstd::functionのようなものを実装する
·2037 文字
技術解説 C++
BSD socketでclient側のport番号を固定する
·321 文字
技術解説 C++