C++は興味深い、でも難しい。タイトルの件について昨日悩み、詳しい人に相談し、解決したので、まとめました。
(2017/05/12追記) 同じ話題について、EzoeRyouさんの解説が出ています→ constexpr ifの落とし穴
注意書き
※ C++はお勉強中(数週間程度しか書いたことのない、本当にビギナーです)なので、用語の使い方や意味の説明について、正確/厳密ではない可能性が高いです。
もしどこか間違っていたらごめんなさい(できることなら、コメント欄などでご指摘いただけると嬉しいです)。
やりたいこと
C++17より導入されるはずのif constexpr文を使うと、テンプレートのインスタンス化時(コンパイル時)に、どの実装を生成するかを簡潔に書けるようになる。
template<class T>
int f() {
if constexpr (std::is_same<T, int>{}) {
// Tがintの時の実装
// ...
} else if constexpr (std::is_same<T, float>{}) {
// Tがfloatの時の実装
// ...
}
}
ここで、もしT
がint
orfloat
でない時はコンパイルを失敗させたいとする。つまり
-
T
に渡せる型は任意ではなく限られており、適切な型によってインスタンス化されていなかったなら、コンパイルエラーにしたい - コンパイルエラー時には、適切なエラーメッセージを出せると親切
これらの目標を満たす実装方法を調べた。
あまり良くなさそうな実装
上手くいかない
else節に、常に失敗するstatic_assertを置けば良いのではという発想で実装してみた。
一見するとなんとなく動きそうだが、上手くいかない。
template<class T>
int f() {
if constexpr (std::is_same<T, int>{}) {
// ...
} else if constexpr (std::is_same<T, float>{}) {
// ...
} else {
static_assert(false, "Bad T!!!!");
}
}
常にコンパイルエラーになってしまう。
動くが、やや冗長
if constexprを書き連ねる前にstatic_assertでガードする。
template<class T>
int f() {
static_assert(std::is_same<T, int>{} || std::is_same<T, float>{}, "Bad T!!!!")
if constexpr (std::is_same<T, int>{}) {
// ...
} else if constexpr (std::is_same<T, float>{}) {
// ...
}
}
この実装は期待通りの振る舞いをしてくれる。
ただしifの条件式がいくつもある場合はstatic_assertが膨れてしまうという問題がある。条件式の記述が実質的に2重になっている点も、DRYという観点から好ましくはない。
本筋っぽい実装
C++エキスパートに教わったやり方。まず次のようなエイリアスを定義しておく。
template <typename T>
constexpr bool false_v = false;
false_v
を利用すると、f
は次のように書ける。
template<class T>
int f() {
if constexpr (std::is_same<T, int>{}) {
// ...
} else if constexpr (std::is_same<T, float>{}) {
// ...
} else {
static_assert(false_v<T>, "Bad T!!!!");
}
}
意図的にT
をfalse_v
に渡すことが重要らしい。
コンパイル例: https://wandbox.org/permlink/u7x2kxBmYN1zmYEE
解釈
上記実装で何が起きているかのおおざっぱな理解。
static_assertの評価タイミング
テンプレート引数に依存しないstatic_assertは、テンプレートのインスタンス化前にチェックされる。つまり static_assert(false)
を関数内にそのまま書くのは実質的に
template<class T>
int f() {
static_assert(false);
}
こう書いてしまっているのと変わらない。もちろん常にstatic_assertで落ちてしまう。
static_assertの評価タイミングを期待通りにするには?
今回の例でいうと、static_assert
の評価を、 テンプレートのインスタンス化後まで遅延させる と「そのブロックがインスタンス化された時にのみコンパイルエラーにする」が実現できる。
具体的なやり方としては、f
のテンプレート引数T
が決まらないと評価されないような式を、static_assertに入れてやればよい(ので前述の実装になる)。false_v
が使いもしないテンプレート引数を取るのは、依存関係を作るという意味がある。
まとめ
if constexpr文はあくまでテンプレートのインスタンス化をコントロールするものであって、ある部分のみコンパイル時に評価する類のものではないらしい。微妙なことをやろうとする場合には若干注意が必要そうだった。