なんかズル臭いが、仕方ないといえば仕方ない。
コンセプトがいつまで経っても標準化されないものだから、本来至極まっとうなSFINAEという型推論機構を利用して、今可能な範囲でテンプレートの型制限を行おうとした結果が、このそびえ立つクソのようなテンプレートメタプログラミングの姿なのだ。
SFINAE。
ググれば腐る程出ると思うので技術的な解説はしない(詳しいことを話すとボロが出るだけとかそういうわけではない。決して)。
Substitution Failure Is Not An Error の名前が示す通り、コンパイル時のテンプレート具現化の段において、テンプレート引数の置き換えの失敗を即座にコンパイルエラーとしないという仕様である。
テンプレートメタプログラミングはあまりにキモいから、まずは関数オーバーロード機構を例にとって頭を慣らそう。
コンパイル時、コンパイラはオーバーロードにより候補が複数存在する関数について、その中から文脈に沿うただひとつを選択する必要に迫られる。
もちろん候補の中には与えられた引数に対して不適当なものも存在するだろう。至極当然である。そいつに馬鹿正直に引数を突っ込むことは出来ない。突っ込もうとすればコンパイルエラーである。だから候補から外す。至極当然である。
もちろん最後に一つも残らなければ本当の意味でコンパイルエラーである。この場合はプログラマが阿呆なだけだ。僕もそのひとりである(Clangの方がgccよりもエラーメッセージが解りやすい)。
まったく同じことがテンプレートについても言える。
この場合、引数をテンプレートパラメータに置き換えて考えれば良い。
本来のテンプレートとは、あらゆる型について汎用的な雛形を与えるためのものであるが、まあぶっちゃけ、本当にあらゆる型が突っ込まれては困る場面のほうが多い。
だから想定される型に応じて処理を別々に(そこまでかけ離れた処理になるとは思わないが)記述する。
さてそこで壁にぶち当たるのだ。
これらのテンプレートを如何にして振り分けるのか。
どちらも同じ数のテンプレート引数を要求している。引数の形式も同じである。
答えはこうだ。
片方が型推論に失敗するようにしてしまえば良い。
片方が(型推論上の意味で)エラーとなったところで、Substitution Failed Is Not An Error な訳だから、そいつを候補から外して型推論は続行、めでたく望みのテンプレートが具現化される。
……というのがC++におけるSFINAEの大まかな僕の理解だ。
そして問題はその「如何にしてエラーを起こさせるか」に移るわけだ。
タイトルのトンチくせえという文句もここから発生している。
一応断っておくと、この記事を書いている時点ではある程度の納得(諦めともいう)に至っているため、SFINAEを利用したコードを見てもそこまで違和感は覚えない。
さて、では問題(というにはあまりにもポピュラーな議題であるが)のコードがこちら。
とある型
T が
T::value_type を持っているかを判定したい、そんな要望に応えるクラスである。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <type_traits> template <typename T, typename = void> class has_value_type : public std::false_type {}; template <typename T> class has_value_type<T, typename std::conditional<false, typename T::value_type, void>::type> : public std::true_type {}; |
動作は確認済み。
下は上のテンプレート特殊化。
俺が納得行かなかったのは
std::conditional の部分である。
だってほら、第1テンプレート引数が
false なもんだから、第3引数の
void が適用されるじゃん、それって上の
false_type に帰着するんじゃねえの、と。
その考えは正しい。
が、2つのテンプレートが等価となることは決してない。
ポイントは3点である。
1、オーバーロードの俎上に載るということは、パッと見、帰着する形が同じであるということである。
2、テンプレートは解釈出来そうなやつを片っ端から解釈しにかかる。
3、テンプレートの具現化は特殊化のほうが優先度が高い。
順を追って見ていこう。
まず、「結局同じ所に行き着くじゃねえか」という疑問は至って正しい。
コンパイラも同様の考えに至る(という書き方はいささか擬人化が過ぎるか)。
そこで上下2つの
has_value_type がオーバーロードの俎上に載るのだ。
これがポイント1。
次、一見して明らかに入らないと思われる条件である、
std::conditional が
true の項、つまり第2引数。
テンプレートはコイツも解釈しにかかるのだ。
これがポイント2。
型
T が
value_type を持たない場合、コンパイラは「あれ、
T::value_type 呼べねえじゃん」となりエラー、しかしSFINAEにより続行、特にはねる要素もない上側の
has_value_typeがめでたく採用される。
つまり、
has_value_type<T>::value == std::false_type::value である。
では型
T が
value_type を持つ場合。
特にエラーが起こらないまま解釈は終了し、
has_value_type は上下共に同値、つまり、まだ一つに絞り込めていない。
そこでポイント3。
この二者、テンプレート特殊化である下側の方が優先度が高いのだ。
よって、下側の
has_value_type が採用。
つまり、
has_value_type<T>::value == std::true_type::value である。
……とまあ、聞いてしまえばなるほどそうかという内容ではある。
しかしこれがC++テンプレートメタプログラミング入門への第一歩、なのだろう。
言語を上手く駆使して解決している、というよりは標準化委員会の揚げ足をとっているような感じがするのが何だか気に入らない点である。
結局のところ、何が言いたいかというと、「さっさとコンセプト標準化せぇや」ということである。当分はこのSFINAEの悪用に世話にならないといけないのだろうが。
追記
C++14 に移行して、テンプレートメタプログラミングへの理解も深まった結果、もう少し綺麗に has_value_type を書き直せたので追記しておく。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class has_value_type_ { public: template <typename T, typename = typename T::value_type> static constexpr decltype(auto) check(T&&) noexcept { return std::true_type {}; } template <typename T> static constexpr decltype(auto) check(...) noexcept { return std::false_type {}; } }; template <typename T> class has_value_type : public decltype(has_value_type_::check<T>(std::declval<T>())) {}; |
C++11 より追加された decltype キーワードを利用してすっきり書き直せた。
意味さえ同じであればいいので、 class ではなく struct でいいとか、そもそも
decltype を使っているのだから
has_value_type_::check<T>() の定義は必要ないとか、まあ書き直せる部分は沢山あるのだが、こういう書き方をしているのは単にこの方が見た目が好きだからというのが大きい。
(それと定義なしで運用した場合、コンパイラの「定義が無い関数を呼んでるぜ」のワーニングが鬱陶しい)