针对类中特定成员函数的检测其实在工作中也可能用到。C++中可以用SFINAE
技巧达到这个目的。
SFINAE是Substitution Failure Is Not An Error的缩写,直译为:匹配失败不是错误。属于C++模板编程中的高级技巧,但属于模板元编程中的基本技巧。当然我其实也并不是C++元编程方面的专家,只是搜集过一些常见的实现方式,然后做过一些测试。在这个过程中,我发现有些常见的SFINAE
写法是有问题的,下面探讨一下。
举个例子,我们来check一下C++标准库的类中有没有push_back()
成员函数。在C++11之前,可以这样写,经过测试是没有问题的:
#include <iostream> #include <map> #include <list> #include <set> #include <string> #include <vector> struct has_push_back { template <typename C, void (C::*)(const typename C::value_type&)> struct Helper; template <typename C, void (C::*)(typename C::value_type)> struct Helper2; template <typename C> static bool test(...) { return false; } template <typename C> static bool test(Helper<C, &C::push_back>*) { return true; } template <typename C> static bool test(Helper2<C, &C::push_back>*) { return true; } }; int main() { std::cout << has_push_back::test<std::list<int> >(NULL) << std::endl; std::cout << has_push_back::test<std::map<int, int> >(NULL) << std::endl; std::cout << has_push_back::test<std::set<int> >(NULL) << std::endl; std::cout << has_push_back::test<std::string>(NULL) << std::endl; std::cout << has_push_back::test<std::vector<int> >(NULL) << std::endl; return 0; }
SFINAE实现方式有很多种,细节处可能不同。
两个Helper类的模板参数中。第二个参数为 push_back的函数指针类型。之所以弄了两个Helper,是因为std::string的push_back的参数为char。也就是value_type类型。而其他STL容器。则是const value_type&
。所以才用了两个Helper。如果是检测其他成员函数,比如size则不需要这么麻烦只要一个Helper即可。
而test函数,对于返回true的模板函数,其参数是一个指针类型。所以实际check的时候,传入一个NULL就可以匹配到。
C++11之后:
#include <iostream> #include <list> #include <map> #include <set> #include <string> #include <vector> template <typename> using void_t = void; template <typename T, typename V = void> struct has_push_back:std::false_type {}; template <typename T> struct has_push_back<T, void_t<decltype(std::declval<T>().push_back(std::declval<typename T::value_type>()))>>:std::true_type {}; int main() { std::cout << has_push_back<std::list<int>>::value << std::endl; std::cout << has_push_back<std::map<int, int>>::value << std::endl; std::cout << has_push_back<std::set<int>>::value << std::endl; std::cout << has_push_back<std::string>::value << std::endl; std::cout << has_push_back<std::vector<int>>::value << std::endl; return 0; }
C++11简洁了许多。如果需求是要检测任意成员函数,而不限定是哪个函数的话,毫无疑问,需要借助宏了。将上面的代码改变成宏的版本,push_back作为宏的一个参数,即可。
我这里为什么用push_back()
举例呢?因为网上能找到的各种SFINAE的实现版本中,很多对于push_back的检测都是有问题的。 而以上列举这两种,都能准确检测出string、vector、list中的push_back()
。当然C++11之前的版本,需要你能枚举出push_back的各种参数种类才行,若待检测的成员函数重载版本比较多的时候,则可能很麻烦。所以还是C++11之后的版本简洁且通用。
下面列举一个常见但某些情况下会存在问题的SFINAE
范本:
class Base { }; class Drive:Base { public: void hello() {} }; template <typename T> struct has_hello { typedef char Yes[1]; // 或 typedef int8_t Yes; typedef char No[2]; // 或 typedef int16_t No; template <typename> static No& has(...); template <typename C> static Yes& has(decltype(&C::hello)); static const bool value = sizeof(has<T>(NULL)) == sizeof(Yes); }; int main() { std::cout << has_hello<Base>::value << std::endl; std::cout << has_hello<Drive>::value << std::endl; }
OK,这个用来检测类中是否有hello成员函数是可以的。但是改变成push_back的版本则有问题。
#include <iostream> #include <list> #include <map> #include <set> #include <string> #include <vector> // 错误的示范 template <typename T> struct has_push_back { typedef char Yes[1]; typedef char No[2]; template <typename> static No& has(...); template <typename C> static Yes& has(decltype(&C::push_back)); static const bool value = sizeof(has<T>(NULL)) == sizeof(Yes); }; int main() { std::cout << has_push_back<std::list<int> >::value << std::endl; std::cout << has_push_back<std::map<int, int> >::value << std::endl; std::cout << has_push_back<std::set<int> >::value << std::endl; std::cout << has_push_back<std::string>::value << std::endl; std::cout << has_push_back<std::vector<int> >::value << std::endl; return 0; }
上面这个是一个典型的有问题的案例——常见范本改变的push_back检测,对上面这几个类,只有string能判断为true。vector、list都check有误。
该版本也有很多其他变种。所谓变种主要是在has的返回值、value的判断方面做改编。也有一定问题,具体大家自己测试吧。