ややプログラム紀行

博士2年のプログラムに関する日記

テンプレートってなに?ってところからSFINAEあたりまで頑張る

この記事はIS18er Advent Calendar 2017の5日目の記事として書かれました。


なんと5日目と6日目、連続してアドベントカレンダーに登録しましたが、一応テーマ的にも連続しているつもりです

もともと「テンプレートの勉強になりそうな記事を書きたいな🤗」とのモチベからstd::functionを読んでみようと思い立ったのですが、いきなり読んだ内容をぶち込むのは流石にアレなので、予習&復習を兼ねてまずはテンプレートの下地を整える記事を書いてみることにしました

ちなみに、この記事はほとんどC++テンプレートテクニックという本の要約になってます(´・ω・`)
std::functionを読む上で必要っぽそうなところを要約しました


目次

テンプレートis何?

テンプレートとは上記の本の言葉を借りると「コンパイラがサポートする超高性能マクロおよび型推論機構」だそうです、なんだか難しいので使用例をみてみます

template <class T>
T add(T x, T y) {
	return x + y;
}

こんな風な関数を用意して、使うときに

add(10, 20);

のようにすると、コンパイラが勝手に引数である10と20の型を推論して、型Tの部分をintなどに置き換えてくれます
これのおかげでわざわざintバージョンのadd、floatバージョンのadd、…と同じ処理の関数を何個も書かなくて済むわけです、だいたいテンプレートの雰囲気は分かったでしょうか

上のようなテンプレートを関数テンプレートと言い、他にもクラステンプレートやメンバテンプレートなど*1もありますが、どれも基本的な考えは上と同じで、抽象的な型を用いてコードを書いたのち、コンパイル時に具体的な型に置き換えることができる機構です

メンバテンプレートは上の関数テンプレートがクラスのメンバ関数にも使えるよという話なので省略しますが、クラステンプレートは少し違うので見てみます

クラステンプレート

std::vectorを使ったことのある人は「vector<int>」なんてコードを書いたことあると思いますがあれはクラステンプレートで実現されています

template <class T>
class Hoge {
	T x;

public:
	void setX(T _x) { x = _x; }
	T getX() { return x; }
};

int main() {
	Hoge<int> dhoge;
	dhoge.setX(1.5);
	cout<<"Hoge<int>:"<<dhoge.getX()<<endl;

	Hoge<float> fhoge;
	fhoge.setX(1.5);
	cout<<"Hoge<float>:"<<fhoge.getX()<<endl;
}

こんなコードを書いてみると、
出力:

Hoge<int>:1
Hoge<float>:1.5

こんな感じになるわけです、簡単ですね😊
ちなみにテンプレートでは型以外に整数値を扱うこともできます

以上がテンプレートの基本です
注意事項として、テンプレートは通常、宣言と定義を同じヘッダーファイルに書くようにしましょう*2
あと、Two-Phase Name Lookupというテンプレートの機構により、たまに型であることを明示するためにtypenameというものを使いますが、気になる人はググってみてください(丸投げ

特殊化

マジで本と同じことしか書いてなくて震えてますがテンプレートの特殊化を説明します

テンプレートは上のように型を抽象化することができますが、具体的にこの型だった時はこの処理にする!なんてこともできます
やり方は本当に簡単で

template<class T>
void print(T x) {
	cout<<"intじゃない"<<endl;
}

template<>
void print(int x) {
	cout<<"int"<<endl;
}

int main() {
	print(10.0);
	print(10);
}

出力:

intじゃない
int

こんな感じです
クラステンプレートも似たような感じです*3

上のような特殊化を明示的特殊化と言います、明示的〜ってやつ多いですね
これを使うとコンパイル時に階乗が計算できちゃいます

template <int N>
struct Factorial {
	static const int value = N * Factorial<N - 1>::value;
};

template <>
struct Factorial<0> {
	static const int value = 1;
};

int main() {
	cout<<Factorial<10>::value<<endl;
}

出力:

3628800

メタプログラミングっぽくなってきました


他には部分特殊化というものもあります
明示的特殊化では、テンプレートの型Tを具体的に決めていましたが、型がポインターだったらこうする〜なんて書き方もできます
型Tを完全に指定して処理を書くわけじゃないので部分的特殊化というわけですね
これを使えば前回の記事で説明したstd::moveを実装することができます!*4

template<class T>
typename remove_reference<T>::type&&
move(T&& t)
{
	typedef typename remove_reference<T>::type U;
	return static_cast<U&&>(t);
}

std::moveを簡略化して載せました
引数は&&なので右辺値参照と思いそうですが、これはユニバーサル参照と言って、引数が左辺値の時は左辺値参照、右辺値の時は右辺値参照になります、初見殺しすぎる
そのあとはremove_referenceで右辺値であれ左辺値であれ参照を外した型Uを定義したのち、改めて引数をU&&にキャストして返していますね

じゃあremove_referenceってなんやねん!っていうと、これが部分特殊化で実現できるわけです

template <class T> struct remove_reference        {typedef T type;};
template <class T> struct remove_reference<T&>  {typedef T type;};
template <class T> struct remove_reference<T&&> {typedef T type;};

値渡し、左辺値参照、右辺値参照の全パターンが網羅されていますが、まさしく部分特殊化ですよね
どの場合であれremove_reference::typeには参照を外した型が定義されます

SFINAE

思ったより早くSFINAEまで来ましたが、ここら辺からちょっと黒魔術っぽい気がします
SFINAE(スフィネ)とはSubstitution Failure Is Not An Error(置き換え失敗はエラーではない)の略だそう、なんかかっこいい…

内容としては「テンプレートをインスタンス化する際に型の置き換えが失敗しても、そこでコンパイルエラーにせず他の候補を探してみる」という機構のことです


f:id:dora119:20171203173916p:plain:w300


具体的なコードを見てみます

struct Test{
	typedef int Value;
};

template<class T>
bool hasValue(typename T::Value) {
	return true;
}

template<class T>
bool hasValue(T) {
	return false;
}

int main() {
	Test test;
	cout<<hasValue<Test>(0)<<endl;
	cout<<hasValue<int>(0)<<endl;
}

出力:

1
0

hasValue<Test>(0)は上のhasValueが、hasValue<int>(0)は下のhasValueが呼ばれていますね
これはどういうことかというと、

  • hasValue<Test>(0)の時はTest構造体はValue型を持っているので上のhasValueが無事呼び出される
  • 一方、hasValue<int>(0)では当然int::Valueなんて存在しないので上のhasValue関数にはマッチしないが、SFINAEのおかげでコンパイルエラーにならず下のhasValue関数が採用される

という仕組みです
細かいことですが、hasValue関数の引数には型しか指定されていませんが、これは無名引数と言って関数内で使わない時に用いられます

enable_if

早速SFINAEを利用したコードを見てみますが、まずはenable_ifを紹介したいと思います

template<bool B, class T = void>
struct enable_if {};
 
template<class T>
struct enable_if<true, T> { typedef T type; };

めっちゃ単純ですね*5、これはテンプレートの明示的特殊化で、bool Bがtrueの時だけenable_if::typeが定義されるそうです

これとSFINAEを組み合わせてみます

template<class T>
void intCheck(T x, typename enable_if<std::is_integral<T>::value>::type* = 0) {
	cout<<"xは整数"<<endl;
}

std::is_initegralというのはTが整数の時に::valueがtrue、そうでなければfalseになるテンプレートです
大体予想できているかもしれませんが、intCheck(10)のように呼び出すと、引数10は整数なのでenable_if::typeが存在し、"xは整数"と出力されます
一方intCheck(1.5)のようにするとenable_if::typeは存在せずエラーになるというわけです

こんな風にしてテンプレート版if文ができちゃいました、SFINAE強い

まとめ

特殊化とSFINAEがすごい

とりあえずこれでテンプレートの触りは説明したので黒魔法使い見習いくらいにはなれたってことですかね🤔
std::functionの全体像を掴めるくらいにはなってたらいいなと思います*6
C++は闇が深すぎるのでメタプログラミングは実装を見ているだけで勉強になりますが、如何せん読みづらすぎですよね

*1:エイリアステンプレートと可変長引数テンプレートは省略します

*2:これはテンプレートのインスタンス化のタイミングに関する問題で、気になる人は「明示的インスタンス化」とか調べてみてください

*3:今回はstd::functionを読めるようになることが目標なので省略しました

*4:C++11以降の話

*5:class T = voidはデフォルトテンプレート引数です

*6:自分がね