こんにちは、田原です。
前回は部分的特殊化する際によく使われる部品と、それらを使った部分特殊化の例を1つ上げました。そして、実はC++11以降、標準ライブラリ<type_traits>で非常に多数の便利な部品が提供されています。今回はその中からのほんの一部について仕組みと使い方を解説します。この辺りが分かれば、それ以外の比較的理解し易いものについてはリファレンスを見れば使えるようになると思います。
ところで、解説しないものの中には実は私もよく理解できていないものがあります。trivial(自明の)という概念をあまり理解できていません。定義を見ると超複雑なのです。memcpy()できるようなクラスのことをtrivially copyable classと呼ぶらしいです。
しかし、C++では結構普通に代入演算子が自動生成されますので、memcpyが必要になるケースは稀と思います。もし必要になったら<type_traits>のis_trivialで始まる機能群を調べてみて下さい。
2018年2月3日追記
これらのような関数テンプレート(コンパイル時処理を目的とするもの)は、メタ関数と呼ばれています。Twitterでお世話になっている「いなむのみたま」さんがQiitaにてメタ関数を纏められていました。
メタ関数について、より網羅的に纏められています。より広く把握されたい方は是非参照されて下さい。きっと知的好奇心を刺激されると思います。
1.char*とchar const*を纏めて部分特殊化したい
前々回、max関数テンプレートをオーバーロードする時、char*とchar const*用のオーバーロードをそれぞれ記述しました。中身が1行しかないのでスルーされた方も多いと思いますが、max関数の場合、内容は本質的に同じものですから1つの定義に纏めるべきです。(同じものが複数あると修正がたいへんです。2つあると単純に倍ですね。テストまで考えると憂鬱です。自動テストを書いていた場合、更に憂鬱です。)
このような時に部分特殊化してまとめるとそれらの手間とリスクを軽減できます。
そこで、まずはクラス・テンプレートの部分特殊化にて進めたいと思います。Herb Sutter氏のアドバイスに従って作ったmax関数テンプレートをクラス・テンプレートのstatic関数で実装したバージョンをターゲットとします。
2.方針
対応する方法は幾つか考えられます。
例えば、char*にconstを挿入してchar const*にしてから分岐することが考えられます。
前々回の例のクラス・テンプレート版では、一度関数テンプレートで受けてからクラス・テンプレートを呼び出していました。これは、関数テンプレートの型推論機能を使って、一々型名を書かないで済むようにすむための工夫です。
その関数テンプレートtemplate<typename T> T max(T iLhs, T iRhs)をchar*でオーバーロードして、Templateをchar const *を指定して実体化すればOKです。
char const* max(char* iLhs, char* iRhs)
{
return Template<char const*>::max(iLhs, iRhs);
}
上記ではchar const*で完全特殊化(=明示的特殊化)されたTemplateを明示的実体化して使っていますので、そのメンバ関数であるstatic char const* max(char const* a, char const* b)が呼ばれます。
恐らくこれがベストに近い手法と思います。しかし、今は部分特殊化の解説ですので、ちょっと無理矢理っぽいですが部分特殊化で解決することにします。
記述が長くなりますが仕組みを理解し易いので、単純明快にテンプレート仮引数のtTypeが、char*、もしくは、char const*である場合に対して部分特殊化を定義することにします。
3.まずはis_sameクラス・テンプレート
前回でてきたテクニックの1を使います。2つの型を指定して同じ型ならtrue、異なる型ならfalseとなるメンバ定数valueを定義します。
#include <iostream>
template<typename tLhs, typename tRhs>
struct is_same
{
constexpr static bool value = false;
};
template<typename tType>
struct is_same<tType, tType>
{
constexpr static bool value = true;
};
int main()
{
std::cout << is_same<int, int>::value << "\n";
std::cout << is_same<int, short>::value << "\n";
}
1 0
単純ですね。is_same<tType, tType>は2つの型が同じ場合である時のみマッチしますので、その時、こちらの部分特殊化が有効になり、そうでない時は、プライマリ・テンプレートが有効になります。前者はvalueをtrue、後者はvalueをfalseとして定義しているため、判定結果がvalueに入ります。
メンバ関数も変数もないクラスって不思議ですよね。どのテンプレートを実体化する(マシン語へ変換する)のか、コンパイル時に決定されますから、プログラム実行時の機能は部分特殊化の範囲記述には使いませんのでメンバ関数や変数を使うことは比較的少ないです。
とはいえ、コンパイル時の動作に影響する場合もあるので全く使わないわけではないです。使う時は不思議な使い方になります。実行時の機能を実行しないまま使うのですから。
これと同様なstd::is_sameクラス・テンプレートが<type_traits>で定義されていますが、上記とは少し異なり、std::true_type, std::false_typeというクラスを使って定義されています。
次のようなイメージです。
struct false_type
{
constexpr static bool value = false;
};
struct true_type
{
constexpr static bool value = true;
};
template<typename tLhs, typename tRhs>
struct is_same : public false_type
{ };
template<typename tType>
struct is_same<tType, tType> : public true_type
{ };
元のものと機能は同じですがvalueのタイプミスを防げるので、このような定義をした方が好ましいです。
もし、部分特殊化側のvalueをタイプミスしてvauleとなっていても、is_sameに一致するクラスを与えたものが無かった場合、エラーになりません。いつかは顕在化するのは確実ですが、タイプミスを見つけるのはできるだけ早い方が良いです。
std::false_typeをstd::fasle_typeなどとタイプミスしたら直ぐにコンパイル・エラーになります。つまり、valueのタイプミスの潜在化を抑止しつつ、自分のタイプミスは直ぐに顕在化するわけです。
#include <iostream>
template<typename tLhs, typename tRhs>
struct is_same
{
constexpr static bool value = false;
};
template<typename tType>
struct is_same<tType, tType>
{
constexpr static bool vaule = true;
};
int main()
{
// std::cout << is_same<int, int>::value << "\n"; // error: 'value' is not a member of 'is_same<int, int>'
std::cout << is_same<int, short>::value << "\n";
}
Wandboxで確認する。(コメントアウトを外して見て下さい。)
さて、実際にはもう少し便利なメンバが定義されています。更にstd::true_type, std::false_typeはstd::integral_constant<>クラス・テンプレートを明示的実体化してtypedefしたものです。
そのstd::integral_constant<>の本質的な部分は前回の Any<>クラステンプレートと同じです。
ポイントだけ抜き出すと次のイメージです。(Anyの名前をintegral_constantへ変えてます。)
template<typename tType, tType kValue>
struct integral_constant
{
constexpr static tType value = kValue;
};
typedef integral_constant<bool, true> true_type;
typedef integral_constant<bool, false> false_type;
<type_traits>をインクルードすることで、これらを更に便利したものが、std名前空間に定義されます。
#include <iostream>
#include <type_traits>
int main()
{
std::cout << std::is_same<int, int>::value << "\n";
std::cout << std::is_same<int, short>::value << "\n";
}
次のリンク先に「可能な実装」としてそれぞれの代表的な実装が記載されています。短いですので一度見ておくと良いと思います。
std::integral_constant, std::true_type, std::false_type
std::is_same
std::integral_constantで使われている下記がちょっと難しいので補足します。
constexpr operator value_type() const { return value; }
- これはキャスト演算子の定義です。
- constexprが付いているのでコンパイル時に実行され結果がコンパイル時定数になると理解下さい。
(constexprが付いている関数でもパラメータがコンパイル時定数でない時は実行時に実行されます。) - 後ろのconstは入門編第32回で説明したconstです。
(たった1つの文に結構難しいことが詰め込んでありますね。)
このキャスト演算子によりstd::true_typeのインスタンスをbool型へ暗黙の型変換出来るようになります。
#include <iostream>
#include <type_traits>
int main()
{
std::true_type aThisIsTrue;
if (aThisIsTrue)
std::cout << "This is true.\n";
}
This is true.
std::true_typeはクラスです。aThisIsTrueはstd::true_typeクラスのインスタンスです。C++標準規格は型も変数も関数も全てスネーク・ケース(小文字、単語間を _ で区切る)で記述するのでテンプレートを扱っている時はなかなか混乱しがちです。各名前が何なのか注意深くチェックして下さい。
4.次にenable_ifクラス・テンプレート
enable_ifは非常によく使います。私はちょっと複雑な部分特殊化やオーバーロードする時の90%くらいで使っている気がします。
部分特殊化の場合、プライマリー・テンプレートの最後にclass tEnable=voidと書き、部分特殊化の定義で型の範囲をenable_if<条件>で定めるのが「定形パターン」なのです。
でも、中身は実に簡単です。
#include <iostream>
#include "typename.h"
template<bool tCond, typename tType = void>
struct enable_if { };
template<typename tType>
struct enable_if<true, tType>
{
typedef tType type;
};
int main()
{
std::cout << TYPENAME(typename enable_if<true>::type) << "\n";
// std::cout << TYPENAME(typename enable_if<false>::type) << "\n";
}
Wandboxで確認する。(コメントアウトを外して見て下さい。)
ポイントは、tCondにfalseを与えるとプライマリー・テンプレートが実体化されますが、これにはtypeが定義されていません。ですので、上記のコメントアウトしている文はエラーになります。
そして、SFINAEの出番です。テンプレートの選択時であれば、上記のようにエラーが発生しても無視されます。エラーになったテンプレートが単に無視され、実体化されません。
5.簡単なサンプル
std::is_sameとstd::enable_ifを使って、char*とchar const*をまとめて部分特殊化します。
#include <iostream>
#include <type_traits>
#include "typename.h"
template<typename tType, class tEnable = void>
struct IsStringImpl
{
static constexpr char const* value="no";
};
template<typename tType>
struct IsStringImpl
<
tType,
typename std::enable_if
<
std::is_same<tType, char*>::value||std::is_same<tType, char const*>::value
>::type
>
{
static constexpr char const* value="yes";
};
template<typename tType>
char const* IsString(tType)
{
std::cout << "[" << TYPENAME(tType) << "] ";
return IsStringImpl<tType>::value;
}
int main()
{
std::cout << IsString("abc") << "\n";
char temp[]="def";
std::cout << IsString(temp) << "\n";
std::cout << IsString(123) << "\n";
}
Wandboxで確認する。
(IsStringImplの部分特殊化、なかなか長いのでインデントしてみました。)
std::is_sameにてtTypeを char*、char const* と比較して一致したら実体化します。
6.maxテンプレートに適用する
前回のmaxテンプレートは、部分特殊化で「ポインタ型」に対応し、明示的特殊化にて「char const*」に対応していました。
char const*は「ポインタ型」でもあるので部分的特殊化にもマッチします。しかし、明示的特殊化と部分的特殊化では明示的特殊化が優先されるので、明示的特殊化が実体化されます。
今回のターゲットは、char const*とchar*の両方にマッチする部分的特殊化をもう一つ定義します。
部分的特殊化同士のため、優先順位がありません。従って、元の部分的特殊化を全ての「ポインタ型」にマッチするままですとchar const*やchar*を与えた時に両方の部分的特殊化にマッチし曖昧(ambiguous)になります。
そこで、1つ目の部分的特殊化は「char const*とchar*以外のポインタ型」を指定し、2つ目の部分的特殊化は「char const*とchar*」を指定します。
// 部分特殊化1
template<typename tType>
struct Template
<
#if 1
tType*,
typename std::enable_if
<
!(std::is_same<tType, char>::value||std::is_same<tType, char const>::value)
>::type
#else
tType*
#endif
>
{
// 略
}
// 部分特殊化2
template<typename tType>
struct Template
<
tType*,
typename std::enable_if
<
std::is_same<tType, char>::value||std::is_same<tType, char const>::value
>::type
>
{
// 略
}
Wandboxで確認する。(#if 1を#if 0へ変更すると部分特殊化1の除外条件が消え曖昧エラーになります。)
7.まとめ
ちょっと分かりにくかったかも知れませんが、今回の主題はstd::enable_ifです。テンプレートを書く時はかなりよく使います。中身は至極単純(falseの時はtypeが未定義)なのですが、未定義だからエラーになるけどSFINAEと共に使うから問題ないという不思議な考え方です。
また、is_sameなど部分特殊化等を行う時の補助的なクラスの定義についても少し解説しました。テンプレートの定義ですから、実行文が事実上ありません。ほとんど宣言文です。なのにconstexpr関数などでコンパイル時に実行される関数がでてきたりして中々ハードと思います。
本質的にコンパイル時に処理できることはコンパイル時に処理してしまった方が、当然プログラムの実行速度はあがります。テンプレートは理解することが非常に難しいのですが、高速でメンテナンス性の高いプログラムを書くために強力なツールです。
さて、今回、部分特殊化の例を示しましたが、条件の記述が(無駄に)長くて読みにくいです。それを改善するのに有用なものがあります。エイリアス・テンプレートはC++11で導入されています。それを使ったstd::enable_if_tと変数テンプレートがC++14で導入されました。そして、変数テンプレートを使ってstd::is_same_vがC++17で導入されます。
次回は、この辺の事情について解説したいと思います。お楽しみに。
