こんにちは。田原です。

前回はクラス・テンプレートの少し高度な部分特殊化の方法を解説しました。今回はこれの関数テンプレート版を解説します。とは言っても関数テンプレートには部分特殊化はありません。その代わりオーバーロードがあります。それを使ってプライマリー・テンプレートをオーバーロードする手法で部分特殊化的な機能を実現できます。その定型パターンを解説します。(実は私、定形パターンは使えますが自由自在にオーバーロードを使えるかというとちょっと無理かも。やはりテンプレートは難しいです。)

1.まずは考え方から

クラス・テンプレートは専用で「部分特殊化」する構文が使えました。同じように関数テンプレートを部分特殊化しようとしてもエラーになります。

template<typename tType>
void foo(tType) { }

template<typename tType>
void foo<tType*>(tType*) { }

をclangでコンパイルすると、「error: function template partial specialization is not allowed」(関数テンプレートは部分特殊化できない)と言われます。

因みにgccでは「error: non-type partial specialization ‘foo‘ is not allowed」(非型部分特殊化は許されない)と言われます。「非型パラメータ」を使ってないのでちょっと混乱してしまいますね。

しかし、クラスと異なり関数は引数があるので引数の型違いで同じ名前の関数を複数定義(オーバーロード)できます。これは関数テンプレートでも同じです。つまり、同じ名前の関数テンプレートのプライマリー・テンプレートを引数を変えて複数定義できるわけです。(なお、当然ですがオーバーロードできないクラス・テンプレートはプライマリー・テンプレートを複数定義することはできません。)

例えば次のような例ではオーバーロードが曖昧になる心配はないので話は簡単ですが、原理はこのようなオーバーロードです。

#include <iostream>

template<typename T>
void foo(T)
{
    std::cout << "foo(" << TYPENAME(T) << ")\n";
}

template<typename T, typename U>
void foo(T, U)
{
    std::cout << "foo(" << TYPENAME(T) << ", " << TYPENAME(U) << ")\n";
}

int main(void)
{
    foo(123);
    foo(123, 456);

    foo("123");
    foo("123", 456);
}
foo(int)
foo(int, int)
foo(char const*)
foo(char const*, int)

2.引数の数が同じ場合

先の例のように引数の数が違う時は難しいことはないですね。プログラマもコンパイラも判断に迷いようがありませんから。
しかし、引数の数が同じ場合はそうもいきません。例えば、引数が一つしか無い時、char型へのポインタとchar const型へのポインタは特別扱いしたいとします。短い関数なら、それぞれについてオーバーロードするのが適切でした
しかし、長くて中身が同じ関数が複数あるとまとめたいものですね。クラス・テンプレートの時は部分特殊化でまとめました。関数テンプレートではプライマリー・テンプレートをオーバーロードしてまとめます。

std::enable_if<true>::typeは定義されています。そしてstd::enable_if<false>::typeは未定義です。クラス・テンプレートの部分特殊化では型が条件に当てはまる場合に前者となり、条件に当てはまらない場合は後者になるよう条件をstd::is_sameなどを使って記述し、「後者ならエラー→SFINAEでコンパイルエラーにせず単に無視→条件に当てはまるものが選択される。」というロジックで実体化するものを決定していました。
関数テンプレートも考え方は同じです。std::enable_ifを使って条件に当てはまらないものがエラーになるよう記述し、SFINAEで候補から外します。
ただし、部分特殊化の構文は使えませんのでプライマリー・テンプレートの構文だけで記述する必要があります。

3.そこで非型パラメータの出番です

第6回で解説したように非型パラメータの「型」はそれより前に出てきたテンプレート・パラメータを使って決めることができます。

こんな感じです。

#include <iostream>
 
template<typename tType, tType kValue>
struct Any
{
    constexpr static tType value = kValue;
};
 
int main()
{
    typedef Any<int, 123> Int123;
    std::cout << "Int123::value=" << Int123::value << "\n";
 
    typedef Any<bool, false> BoolFalse;
    std::cout << "BoolFalse::value=" << BoolFalse::value << "\n";
}

Anyクラス・テンプレートのkValueはその直前にあるテンプレート・パラメータtType型です。これは関数テンプレートでも同じように使えます。

#include <iostream>
#include "typename.h"
 
template<typename T, T kValue>
void foo()
{
    std::cout << "foo<" << TYPENAME(T) << ", " << kValue << ">()\n";
};
 
int main()
{
    foo<int, 123>();
    foo<char, 'X'>();
}
foo<int, 123>()
foo<char, X>()

さて、このkValueの型 T ですが、単純な T である必要が無いのです。T*T const&等など加工できます。
逆に T がポインタ型だったらポインタを外した型にしてみます。std::remove_pointer<T>::typeを使います。これは T がポインタ型ならポインタを外した部分、Tがポインタ型でなければそのままとなります。

#include <iostream>
#include <type_traits>
#include "typename.h"

int main()
{
    std::cout << TYPENAME(typename std::remove_pointer<int>::type) << "\n";
    std::cout << TYPENAME(typename std::remove_pointer<int*>::type) << "\n";
}
int
int

2行目の型はint*ですが結果はintになってます。このようにstd::remove_pointer<T>::typeのtypeはTからポインタを外した型がtypedefされています。(ここのPossible implementationに定義例があります。constやvolatile対応しているのでややこしく見えますが、先頭の2行が本質的な部分です。)

これを使って先程のkValueを T もしくは、ポインタを外した T で定義してみました。

#include <iostream>
#include "typename.h"
  
template<typename T, typename std::remove_pointer<T>::type kValue>
void foo()
{
    std::cout << "foo<" << TYPENAME(T) << ", " << kValue << ">()\n";
};
  
int main()
{
    foo<int, 123>();
    foo<int*, 456>();
}
foo<int, 123>()
foo<int*, 456>()

4.そしていよいよSFINAEの出番です

先程のstd::remove_pointerの実体化時にエラーが起きたら、SFINAEの効果でそのテンプレート(オーバーロード)が選択されません。ならば、そこでenable_ifすれば良いです。

ところで、std::enable_if<true>::typeはvoidです。非型パラメータにvoid型は指定できません。しかし、ポインタ型なら指定できますから、void*型にしています。そして、nullptrはvoid*に渡せますので、呼び出し時にnullptrを指定して使ってます。

 #include <iostream>
 #include "typename.h"
 
 template
 <
     typename T,
     typename std::enable_if
     <
         std::is_same<T, char*>::value || std::is_same<T, char const*>::value
     >::type*
 >
 void foo()
 {
     std::cout << "[0] foo<" << TYPENAME(T) << ">\n";
 }

 template
 <
     typename T,
     typename std::enable_if
     <
         !(std::is_same<T, char*>::value || std::is_same<T, char const*>::value)
     >::type*
 >
 void foo()
 {
     std::cout << "[1] foo<" << TYPENAME(T) << ">\n";
 }
 
 int main()
 {
     foo<int, nullptr>();
     foo<char*, nullptr>();
     foo<char const*, nullptr>();
 }

5.次に型推論を使いましょう

型を明示的に指定してきましたが、大変面倒ですね。関数テンプレートの大きなありがたみは、型推論により型を明示的に指定しなくて済むことです。それを使いましょう。

そのためには、

  1. nullptrを書かないでよいようにする
    これを書かないといけない場合、先頭の型の指定を省略できません。そこで、std::enable_ifで与えられる非型パラメータにデフォルト引数を指定します。普通の関数のデフォルト引数と同様テンプレート・パラメータにデフォルト引数を指定すると明示的な指定を省略できるようになります。

  2. foo()に仮引数を追加する
    型推論で先頭のテンプレート・パラメータを推定させるために必要になります。このサンプルのfoo()関数ではあまり意味ありませんが、型推論のサンプルですので仮引数を追加します。

 #include <iostream>
 #include "typename.h"
 
 template
 <
     typename T,
     typename std::enable_if
     <
         std::is_same<T, char*>::value || std::is_same<T, char const*>::value
     >::type* = nullptr
 >
 void foo(T)
 {
     std::cout << "[0] foo<" << TYPENAME(T) << ">\n";
 }

 template
 <
     typename T,
     typename std::enable_if
     <
         !(std::is_same<T, char*>::value || std::is_same<T, char const*>::value)
     >::type* = nullptr
 >
 void foo(T)
 {
     std::cout << "[1] foo<" << TYPENAME(T) << ">\n";
 }
 
 int main()
 {
     foo(123);
     foo("abc");
 }

Wandboxで確認する。

6.おまけ

さて、概ね5の使い方で問題はでないのですが、一部のコンパイラを使って、かつ、明示的にちょっと特殊なテンプレート・パラメータを指定すると、こっそり嫌なことが起こります。

2番めのテンプレート・パラメータは、void*型の非型パラメータですので「コンパイル時定数」となるアドレスを渡すことができます。例えば、(void*)0はコンパイル時定数となるアドレスです。nullptrとマッチします。そして、これは微妙なのですが(void*)1もコンパイル時定数となる処理系があります。Visual C++やC++17より前のclangです。(もちろん(void*)2(void*)3…なども同様です。)
Wandboxで確認する。(gccの場合)
Wandboxで確認する。(clangの場合)

例えば、こんなことするケースは稀と思いますが、foo<int, (void*)1>(123);として呼び出すとfoo<int, nullptr>(123);とはテンプレート・パラメータが異なるので2つの別の実体化が生成され、無駄にコードが大きくなります。また、それぞれのfooの先頭アドレスも異なる筈ですので、なにか予想外の挙動が発生するリスクがあります。

積極的に防ぐ必要まではないと思いますが、ちょっと記述が長くなる以外のデメリットなくこの問題を回避できる方法がstd::enable_ifを使ってオーバーロードする時、enablerを使う?の記事に記載されています。std::enable_ifで出力する型を void ではなく、std::nullptr_t とするだけです。

 typename std::enable_if
 <
     std::is_same<T, char*>::value || std::is_same<T, char const*>::value
 >::type* = nullptr
 #include <cstddef>

 // 中略

 typename std::enable_if
 <
     std::is_same<T, char*>::value || std::is_same<T, char const*>::value,
     std::nullptr_t
 >::type* = nullptr

std::nullptr_tは、cstddefヘッダでtypedefされている型です。
これはnullptrの型です。私は長い間nullptrの型はvoid*と思い込んで居たのですが、実は違います。
decltype(nullptr)でした。decltypeは(後日解説予定ですが)C++11で追加された重要機能で引数の「型」を返します。つまり、decltype(nullptr)はnullptrの「型」です。そのまんまですね。(笑)
そして、std::nullptr_t型の変数はvoid*型へ暗黙の型変換できます。

この型の最大の特長は、std::nullptr_t型の変数や定数が取る値がnullptrだけしかないことです。その他の値を取るstd::nulptr_t型を作ろうにもコンパイルエラーになるので作れません。
従って、上記対策適用後ならばfoo<int, (std::nulptr_t)1>(123);と書くとコンパイル・エラーになるためfoo<int, nullptr>(123);としか書けません。つまり、想定外の実体化がなされることがないので安心なのです。
Wandboxで確認する。

なお、(void*)1がコンパイル・エラーになるかならないか、コンパイラとそのバージョンによって複雑です。この辺りがよく分からなくて上記記事を書かれた@kazatsuyuさんも居るTwitterにて相談して、その結果を私なりに解釈したものが「6.おまけ」です。

ところで、この相談の過程でstd::enable_ifを使ってオーバーロードする時、enablerを使う?を近いうちに修正するとのことでした。規格上の正しい姿を明確にしたいと思われているようです。(ちょっとたいへんそうです。がんばれ@kazatsuyuさん、遠くからですが応援しています。)

後、非型パラメータとしてポインタを積極的に使用するのはあまりお勧めしません。void*型だけでもコンパイラによる挙動の相違にかなり振り回されました。コンパイル時定数ならばなんでも指定できそうなものですが、どうもそうでもないようで何を指定できて何を指定できないのか、その心がよく理解できませんでした。必要な場面も少ないですし、デバッグで苦労し易い機能ですので、どうしても必要な時以外なるべく使わない方がよさそうです。マルチプラット・フォームや複数のバージョンのコンパイラをサポートする場合は特に。

最後にもう一つ。本文ではnullptrの型はdecltype(nullptr)と書きましたが、これはgccの場合です。clangとVisual C++ではstd::nullptr_tと返ってきます。std::nullptr_tはdecltype(nullptr)をtypedefしたものですから本当の型名を取り出せなかったことになります。つまりnullptrの型の型名は決まっていないということのようです。
仮に決めた場合、誰かがそれと同じ名前をtypedefしているとそのプログラムをコンパイルできなくなります。それは辛いです。そして決める必要もほとんどないので決めなかったということかもしれません。

7.まとめ

今回は、部分特殊化のない関数テンプレートを、プライマリー・テンプレートのオーバーロードで部分特殊化と同様な効果を有無手法を解説しました。クラス・テンプレートと書き方が異なるのでややこしいのですが、原理さえ把握できたら後は定形パターンですので覚えてしまうのが良いかも知れません。(実は私はそうしてます。この辺は本当に難しいです。)

さて、次回はちょっと応用的な利用例として、クラスのメンバの有無をチェックするテンプレートを紹介してみたいと思います。テンプレート自体をマクロで作ります。メタ・メタ・プログラミングですね。名前の通り確かに分かりにくいです。しかし、メンバの有無を判定できたらなと思うことがたまに有りますので取り上げたいと思います。お楽しみに。

なお、12月はちょっと忙しくなりそうです。もしかするとお休みする回があるかも知れません。その時はお知らせにてご連絡致します。