読者です 読者をやめる 読者になる 読者になる

【初心者C++er Advent Calendar 2016 20日目】関数テンプレートの呼び出しに優先順位をつける

C++

初心者C++er Advent Calendar 2016 20日目の記事です。
クリスマスが終わって『もはや Advent Calendar とは…』となってるんですが、まあ埋まってないより埋まってるほうがいいかなーと。
今回は初心者向けかどうか怪しい内容ですが、ちょっとしたハックでも。
なお、この記事は SFINAE を知ってる事が前提の記事になります。

[コード]

さて、次のように SFINAE を利用して『a + aができる型(値)のみ』受け付けたい関数テンプレートを定義したいことがあると思います。

template<typename T>
auto
twice(T a)
// decltype() 内で式を記述することで a + a を要求する
->decltype(a + a){
    return a + a;
}

// 42 + 42 は有効なので OK
twice(42);         // 84

// 3.14f + 3.14f も有効なので OK
twice(3.14f);      // 6.28

// "homu" + "homu" は無効なので NG
twice("homu");

まあここまではよくあるコードだと思います。

[問題]

このような関数テンプレートを定義した場合に『それ以外にマッチする関数』も定義したいことがあると思います。

// a + a が有効でない引数の場合に呼び出す関数を定義
template<typename T>
auto
twice(T t){
    return t;
}

twice("homu");        // "homu"

これでよかったよかった…とはならずに、次のように『a + aができる型(値)』を渡した場合にエラーになります。

// Error: call of overloaded 'twice(int)' is ambiguous
twice(42);

http://melpon.org/wandbox/permlink/GqFem4RxFct7gvqF

これは twice(int) 時に『両方の関数に該当するため』呼び出しが解決できないためです。

[解決]

解決策はいくつかあるんですが、次のようにして『オーバーロードを解決するため引数』を追加することで比較的簡単に回避することが出来ます。

// 引数に int を追加する
template<typename T>
auto
twice(int, T a)
->decltype(a + a){
    return a + a;
}

// 引数に bool を追加する
template<typename T>
auto
twice(bool, T t){
    return t;
}

// int 型の値を関数に渡すことで最初に定義した関数の方を優先して呼び出されるようになる
twice(0, 42);        // 84
twice(0, 3.14f);     // 6.28
twice(0, "homu");    // "homu"

http://melpon.org/wandbox/permlink/WeEUFMV8MGEZRkyb

これは twice(0, 42) の場合は twice(bool, T) よりも twice(int, T) にマッチし、twice(0, "homu") の場合は SFINAE によって twice(int, T)オーバーロード候補から外れ、0bool 型に暗黙の型変換されるので twice(bool, T) にマッチするためです。
まあ、要するに

void func(int);
void func(bool);

// func(int) を呼び出す
func(0);

ということを利用しているだけですね。

[ちょっと改良]

上記のままだと毎回 0 を渡す必要があるのでラッパ関数を用意すると使い勝手がよくなります。

template<typename T>
auto
twice_impl(int, T a)
->decltype(a + a){
    return a + a;
}

template<typename T>
auto
twice_impl(bool, T t){
    return t;
}

template<typename T>
auto
twice(T t){
    // ラッパ関数を用意して、その中で twice の実装を呼び出す
    return twice_impl(0, t);
}

twice(42);        // 84
twice(3.14f);     // 6.28
twice("homu");    // "homu"

http://melpon.org/wandbox/permlink/lwTkvXx39ptEP6TN

これでユーザコードからこのハックを隠蔽する事が出来ました。

[まとめ]

と、言う感じで簡単なハックを紹介してみました。
SFINAE を使った関数を定義しているとこういう『オーバーロードが解決できない関数』が結構出てくるんで地味に重宝します。
もうちょっと汎用的にしたコードが下記の記事に載ってるので気になる方は読んでみるとよいです。

[参照]