基底クラスと派生クラスとの間で、同じ関数名、同じ引数、同じ戻り値のメンバ関数をそれぞれ定義し、SFINAEによってそれらを呼び分けることが出来ないかと考えたことがある。
例えば次のようなコードがあったとする。
struct Base { template <class T, std::enable_if_t<std::is_integral<T>::value, std::nullptr_t> = nullptr> static void func(T x) { std::cout << "func int:" << x << std::endl; } template <class T, std::enable_if_t<std::is_floating_point<T>::value, std::nullptr_t> = nullptr> static void func(T x) { std::cout << "func float:" << x << std::endl; } }; int main() { Base::func(3.1); Base::func(3); }
これは期待通りに動く。ごく普通のSFINAEである。引数が整数型か浮動小数点型かによって、どちらの関数が呼ばれるのかが決定される。
ではこのfuncを、基底クラスと派生クラスに分けて定義した場合はどうなるだろうか。
struct Base { template <class T, std::enable_if_t<std::is_integral<T>::value, std::nullptr_t> = nullptr> static void func(T x) { std::cout << "func int:" << x << std::endl; } }; struct Derived : public Base { using Base::func; template <class T, std::enable_if_t<std::is_floating_point<T>::value, std::nullptr_t> = nullptr> static void func(T x) { std::cout << "func float:" << x << std::endl; } }; int main() { //Derived::func(1); MSVC2015,2017でコンパイルエラーになった。GCCでは何故か動いちゃったが多分バグ。 Derived::func(1.0); return 0; }
この方法は、何故かGCCではコンパイルが通り動作してしまうが、C++の規格通りならコンパイル不可能である。Derived内ではusing Base::funcと宣言されており、あたかもBase::funcが隠蔽されることなく呼び出せるかのように見えるが、実は基底クラスのメンバ関数のusing宣言には例外がある。同じ関数名、同じparameter-type-list、CV修飾、参照修飾を持つ基底クラスのメンバ関数は、たとえusing宣言されていようと隠蔽されるのだ。従って、上のコードにおいてBase::funcはusing宣言を無視して隠蔽される。
これを回避する方法はないわけではない。コードが多少汚くなってもよいのなら、動作に影響しないデフォルト引数をつけてやればいい。例えば下記のようにDerived::funcにstd::nullptr_tという引数を追加してやると、引数型リストが一致しなくなるのでBase::funcの隠蔽が起きなくなるようだ。
struct Derived : public Base { using Base::func; template <class T, std::enable_if_t<std::is_floating_point<T>::value, std::nullptr_t> = nullptr> static void func(T x, std::nullptr_t = nullptr) { std::cout << "func float:" << x << std::endl; } };
この腹立たしい挙動によって、自作Variantの実装に大いに梃子摺った。メンバ関数テンプレートの隠蔽についても同じルールが適用されるのは果たして良い仕様と言えるのだろうか。