深入浅出SFINAE
来源:互联网 发布:淘宝运营主要做什么 编辑:程序博客网 时间:2024/05/18 03:18
转自: http://blog.csdn.net/godcupid/article/details/50420925
翻译的很差,最好去看原文。
原文链接:http://jguegant.github.io/blogs/tech/sfinae-introduction.html
引言
网上看到一篇介绍SFINAE的文章,自己听说过很多遍,但都不太确定到底是个啥东西。作者的文章写的很好,就拿来翻译一下练练手。原作者说看了Louis Dionne 的演讲"C++ Metaprogramming: A Paradigm Shift",对其中的Boost.Hana库中的 is_valid 特性特别感兴趣。应该是有感而发,有了此文。如果不知道啥是SFINAE,并且想知道,那么就去看原文吧。原文写的很好,代码里的注释很多,受益匪浅!
Introspection in C++?
在解释SFINAE是啥之前,我们先来它的主要应用之一:introspection(内省)。c++不擅长在运行时检测对象的类型和属性。虽然c++有 RTTI,但RTTI除了能提供对象的类型之外,也提供不了其他有用的信息了。而动态语言在这方面就非常方便,举个Python的栗子:
class A(object): # Simply overrides the 'object.__str__' method. def __str__(self): return "I am a A"class B(object): # A custom method for my custom objects that I want to serialize. def serialize(self): return "I am a B"class C(object): def __init__(self): # Oups! 'serialize' is not a method. self.serialize = 0 def __str__(self): return "I am a C"def serialize(obj): # Let's check if obj has an attribute called 'serialize'. if hasattr(obj, "serialize"): # Let's check if this 'serialize' attribute is a method. if hasattr(obj.serialize, "__call__"): return obj.serialize() # Else we call the __str__ method. return str(obj)a = A()b = B()c = C()print(serialize(a)) # output: I am a A.print(serialize(b)) # output: I am a B.print(serialize(c)) # output: I am a C.
可以看到,python可以非常方便的查看一个对象是都有某个属性及这个属性的类型。在上面的栗子中,就是检测有没有serialize函数,有就调用,没有就调用str函数。很强大,不是吗?c++也可以做到这些!
下面是个C++14的方法,用到了Boost.Hana 库中的 is_valid:
#include <boost/hana.hpp>#include <iostream>#include <string>using namespace std;namespace hana = boost::hana;// Check if a type has a serialize method.auto hasSerialize = hana::is_valid([](auto&& x) -> decltype(x.serialize()) { });// Serialize any kind of objects.template <typename T>std::string serialize(T const& obj) { return hana::if_(hasSerialize(obj), // Serialize is selected if available! [](auto& x) { return x.serialize(); }, [](auto& x) { return to_string(x); } )(obj);}// Type A with only a to_string overload.struct A {};std::string to_string(const A&){ return "I am a A!";}// Type B with a serialize method.struct B{ std::string serialize() const { return "I am a B!"; }};// Type C with a "wrong" serialize member (not a method) and a to_string overload.struct C{ std::string serialize;};std::string to_string(const C&){ return "I am a C!";}int main() { A a; B b; C c; std::cout << serialize(a) << std::endl; std::cout << serialize(b) << std::endl; std::cout << serialize(c) << std::endl;}
像c++如此复杂的语言,也可以像python一样,很方便的给出serialize的解决方案。不过不像python等动态语言,在运行时获取这些信息,c++的实现是在编译期实现的,这些全靠c++编译器可以在编译期访问类型信息的特性。接下来就看看如何分别使用c++98,c++11,c++14,来创建我们自己的is_valid。
C++98来实现:
c++98的解决方案主要依赖于三个概念:overload resolution, SFINAE 和 the static behavior of sizeof.
Overload resolution:
c++中,如果像"f(obj)"这样一个函数被调用时,会触发编译器的重载解析机制--根据形参找出最合适的名叫f的函数。没啥能比一个好栗子更能说明问题了:
void f(std::string s); // int can't be convert into a string.void f(double d); // int can be implicitly convert into a double, so this version could be selected, but...void f(int i); // ... this version using the type int directly is even more close!f(1); // Call f(int i);
c++中也有可以接受任何参数的sink-hole function(不知道咋翻译)。函数模版就可以接受任何类型的参数(比如 T),但是真正的black-hole function应该是variadic functions。C 的printf就是这样的函数。
std::string f(...); // Variadic functions are so "untyped" that...template <typename T> std::string f(const T& t); // ...this templated function got the precedence!f(1); // Call the templated function version of f.
但是一定要记住,函数模版跟variadic functions是不一样的,函数模版其实是类型安全的(编译时可以检测参数类型对不对)。
SFINAE:
前面说了那么多,现在终于轮到主角登场了。SFINAE是Substitution Failure Is Not An Error的缩略语。substitution可以看作是用实际调用时提供的类型或值来替换模版参数的机制。如果替换后,代码变成了无效代码,编译器也不应该抛出错误,而是继续寻找其他的替换方案。SFINAE这个概念说的正是编译器的这种“神圣”行为。来个栗子:
/* The compiler will try this overload since it's less generic than the variadic. T will be replace by int which gives us void f(const int& t, int::iterator* b = nullptr); int doesn't have an iterator sub-type, but the compiler doesn't throw a bunch of errors. It simply tries the next overload. */template <typename T> void f(const T& t, typename T::iterator* it = nullptr) { }// The sink-hole.void f(...) { }f(1); // Calls void f(...) { }
The operator sizeof:
sizeof操作符可以在编译时返回一个类型或表达式的大小
typedef char type_test[42];type_test& f();// In the following lines f won't even be truly called but we can still access to the size of its return type.// Thanks to the "fake evaluation" of the sizeof operator.char arrayTest[sizeof(f())];std::cout << sizeof(f()) << std::endl; // Output 42.
如果可以在编译期计算一个整型,难道不能在编译期做整型比较吗?当然可以啦!
typedef char yes; // Size: 1 byte.typedef yes no[2]; // Size: 2 bytes.// Two functions using our type with different size.yes& f1();no& f2();std::cout << (sizeof(f1()) == sizeof(f2())) << std::endl; // Output 0.std::cout << (sizeof(f1()) == sizeof(f1())) << std::endl; // Output 1.
现在所有的难点都搞定了,先写个hasSerialize吧:
template <class T> struct hasSerialize{ // For the compile time comparison. typedef char yes[1]; typedef yes no[2]; // This helper struct permits us to check that serialize is truly a method. // The second argument must be of the type of the first. // For instance reallyHas<int, 10> would be substituted by reallyHas<int, int 10> and works! // reallyHas<int, &C::serialize> would be substituted by reallyHas<int, int &C::serialize> and fail! // Note: It only works with integral constants and pointers (so function pointers work). // In our case we check that &C::serialize has the same signature as the first argument! // reallyHas<std::string (C::*)(), &C::serialize> should be substituted by // reallyHas<std::string (C::*)(), std::string (C::*)() &C::serialize> and work! template <typename U, U u> struct reallyHas; // Two overloads for yes: one for the signature of a normal method, one is for the signature of a const method. // We accept a pointer to our helper struct, in order to avoid to instantiate a real instance of this type. // std::string (C::*)() is function pointer declaration. template <typename C> static yes& test(reallyHas<std::string (C::*)(), &C::serialize>* /*unused*/) { } template <typename C> static yes& test(reallyHas<std::string (C::*)() const, &C::serialize>* /*unused*/) { } // The famous C++ sink-hole. // Note that sink-hole must be templated too as we are testing test<T>(0). // If the method serialize isn't available, we will end up in this method. template <typename> static no& test(...) { /* dark matter */ } // The constant used as a return value for the test. // The test is actually done here, thanks to the sizeof compile-time evaluation. static const bool value = sizeof(test<T>(0)) == sizeof(yes);};// Using the struct A, B, C defined in the previous hasSerialize example.std::cout << hasSerialize<A>::value << std::endl;std::cout << hasSerialize<B>::value << std::endl;std::cout << hasSerialize<C>::value << std::endl;
结构体reallyHas使用来保证serialize是一个类成员方法,而不是一个类成员变量或其他东西的。
为了简单,没有考虑仿函数的情况:
struct E{ struct Functor { std::string operator()() { return "I am a E!"; } }; Functor serialize;};E e;std::cout << e.serialize() << std::endl; // Succefully call the functor.std::cout << testHasSerialize(e) << std::endl; // Output 0.
有了hasSerialize,写serialize就是小菜一碟了:
template <class T> std::string serialize(const T& obj){ if (hasSerialize<T>::value) { return obj.serialize(); // error: no member named 'serialize' in 'A'. } else { return to_string(obj); }}A a;serialize(a);
大功告成!!
编译失败!!??没想到吧,呵呵。
将模板展开后是这样的:
std::string serialize(const A& obj){ if (0) { // Dead branching, but the compiler will still consider it! return obj.serialize(); // error: no member named 'serialize' in 'A'. } else { return to_string(obj); }}
看出问题了吧。虽然分支0永远也跑不到,但是编译器还是便以这个分支下的代码的。0的分支,说明obj没有serialize函数,但是却调用了,当然出错了。这个问题如何解决呢?答案就是不用if语句了,而是将这个函数分成两个函数,每个函数对应一个分支。如何分?用enable_if:
template<bool B, class T = void> // Default template version.struct enable_if {}; // This struct doesn't define "type" and the substitution will fail if you try to access it.template<class T> // A specialisation used if the expression is true. struct enable_if<true, T> { typedef T type; }; // This struct do have a "type" and won't fail on access.// Usage:enable_if<true, int>::type t1; // Compiler happy. t's type is int.enable_if<hasSerialize<B>::value, int>::type t2; // Compiler happy. t's type is int.enable_if<false, int>::type t3; // Compiler unhappy. no type named 'type' in 'enable_if<false, int>';enable_if<hasSerialize<A>::value, int>::type t4; // no type named 'type' in 'enable_if<false, int>';
通过enable_if,我们的函数可以分成下面这样两个:
template <class T> typename enable_if<hasSerialize<T>::value, std::string>::type serialize(const T& obj){ return obj.serialize();}template <class T> typename enable_if<!hasSerialize<T>::value, std::string>::type serialize(const T& obj){ return to_string(obj);}A a;B b;C c;// The following lines work like a charm!std::cout << serialize(a) << std::endl;std::cout << serialize(b) << std::endl;std::cout << serialize(c) << std::endl;
C++11来实现:
decltype, declval, auto & co:
decltype 可以给出一个表达式最终的类型:
B b;decltype(b.serialize()) test = "test"; // Evaluate b.serialize(), which is typed as std::string.// Equivalent to std::string test = "test";
declval 主要是为decltype服务的. 有了declval,SFINAE用起来就方便多了。
struct Default { int foo() const {return 1;}};struct NonDefault { NonDefault(const NonDefault&) {} int foo() const {return 1;}};int main(){ decltype(Default().foo()) n1 = 1; // int n1// decltype(NonDefault().foo()) n2 = n1; // error: no default constructor decltype(std::declval<NonDefault>().foo()) n2 = n1; // int n2 std::cout << "n2 = " << n2 << '\n';}
auto相当于c#中的var,有栗子:
bool f();auto test = f(); // Famous usage, auto deduced that test is a boolean, hurray!// vvv t wasn't declare at that point, it will be after as a parameter!template <typename T> decltype(t.serialize()) g(const T& t) { } // Compilation error// Less famous usage:// vvv auto delayed the return type specification!// vvv vvv the return type is specified here and use t!template <typename T> auto g(const T& t) -> decltype(t.serialize()) { } // No compilation error.
Constexpr:
constexpr int factorial(int n){ return n <= 1? 1 : (n * factorial(n - 1));}int i = factorial(5); // Call to a constexpr function.// Will be replace by a good compiler by:// int i = 120;
std::true_type & std::false_type :
struct testStruct : std::true_type { }; // Inherit from the true type.constexpr bool testVar = testStruct(); // Generate a compile-time testStruct.bool test = testStruct::value; // Equivalent to: test = true;test = testVar; // true_type has a constexpr converter operator, equivalent to: test = true;
第一种方案:
回忆一下C++98的方案 ,当我们把reallyHas ,sizeof分别换成 decltype ,constexpr:
template <class T> struct hasSerialize{ // We test if the type has serialize using decltype and declval. template <typename C> static constexpr decltype(std::declval<C>().serialize(), bool()) test(int /* unused */) { // We can return values, thanks to constexpr instead of playing with sizeof. return true; } template <typename C> static constexpr bool test(...) { return false; } // int is used to give the precedence! static constexpr bool value = test<T>(int());};
在 decltype 中,所有的表达式都会被计算,但只有最后的表达式的类型会被认为是最终的类型。在上面的栗子中,前面的表达式仅仅是用来触发Substitution Failure的。
第二种方案:
这种方案用到了 std::true_type 和 std::false_type:
// Primary template, inherit from std::false_type.// ::value will return false. // Note: the second unused template parameter is set to default as std::string!!!template <typename T, typename = std::string>struct hasSerialize : std::false_type{};// Partial template specialisation, inherit from std::true_type.// ::value will return true. template <typename T>struct hasSerialize<T, decltype(std::declval<T>().serialize())> : std::true_type{};
优先使用最合适的,如果大家都一样合适,那么就使用"most specialized"的。
C++14来实现:
auto & lambdas:
C++14 中,auto关键字得到了增强,可以用来表示函数或方法的返回类型了:
auto myFunction() // Automagically figures out that myFunction returns ints.{ return int();}
如果返回类型编译器猜不出来,就不能用了。
C++11 就有 lambdas 了,语法如下:
[capture-list](params) -> non-mandatory-return-type { ...body... }
C++14 加强了 lambdas,使之可以接受auto类型的参数了。 Lambdas是通过匿名类型来实现的,如果一个 lambda 有 auto类型的参数, 那么它的 "Functor operator"operator() 会实现成一个模版:
// ***** Simple lambda unamed type *****auto l4 = [](int a, int b) { return a + b; };std::cout << l4(4, 5) << std::endl; // Output 9.// Equivalent to:struct l4UnamedType{ int operator()(int a, int b) const { return a + b; }};l4UnamedType l4Equivalent = l4UnamedType();std::cout << l4Equivalent(4, 5) << std::endl; // Output 9 too.// ***** auto parameters lambda unnamed type *****// b's type is automagically deduced!auto l5 = [](auto& t) -> decltype(t.serialize()) { return t.serialize(); };std::cout << l5(b) << std::endl; // Output: I am a B!std::cout << l5(a) << std::endl; // Error: no member named 'serialize' in 'A'.// Equivalent to:struct l5UnamedType{ template <typename T> auto operator()(T& t) const -> decltype(t.serialize()) // /!\ This signature is nice for a SFINAE! { return t.serialize(); }};l5UnamedType l5Equivalent = l5UnamedType();std::cout << l5Equivalent(b) << std::endl; // Output: I am a B!std::cout << l5Equivalent(a) << std::endl; // Error: no member named 'serialize' in 'A'.
既然有auto参数的lambda用到了模版,那么SFINAE就可以应用到这里了:
// Check if a type has a serialize method.auto hasSerialize = hana::is_valid([](auto&& x) -> decltype(x.serialize()) { });
hana::is_valid是个函数,这个函数以我们的lambda为参数,返回一个类型。我们给这个返回的类型取名叫容器,这个容器负责将lambda的匿名类型保存起来,方便以后用。我们先看下这个containter的作用:
template <typename UnnamedType> struct container{ // Remembers UnnamedType.};template <typename UnnamedType> constexpr auto is_valid(const UnnamedType& t) { // We used auto for the return type: it will be deduced here. return container<UnnamedType>();}auto test = is_valid([](const auto& t) -> decltype(t.serialize()) {})// Now 'test' remembers the type of the lambda and the signature of its operator()!
接下来我们来实现这个Container,这个container需要有一个操作符operator()函数,我们需要通过这个函数来调用我们的lambda,我们的lambda需要一个参数(就是要检验它有没有serialize成员方法),所以容器的operator()也要有一个参数
template <typename UnnamedType> struct container{// Let's put the test in private.private: // We use std::declval to 'recreate' an object of 'UnnamedType'. // We use std::declval to also 'recreate' an object of type 'Param'. // We can use both of these recreated objects to test the validity! template <typename Param> constexpr auto testValidity(int /* unused */) -> decltype(std::declval<UnnamedType>()(std::declval<Param>()), std::true_type()) { // If substitution didn't fail, we can return a true_type. return std::true_type(); } template <typename Param> constexpr std::false_type testValidity(...) { // Our sink-hole returns a false_type. return std::false_type(); }public: // A public operator() that accept the argument we wish to test onto the UnnamedType. // Notice that the return type is automatic! template <typename Param> constexpr auto operator()(const Param& p) { // The argument is forwarded to one of the two overloads. // The SFINAE on the 'true_type' will come into play to dispatch. // Once again, we use the int for the precedence. return testValidity<Param>(int()); }};template <typename UnnamedType> constexpr auto is_valid(const UnnamedType& t) { // We used auto for the return type: it will be deduced here. return container<UnnamedType>();}// Check if a type has a serialize method.auto hasSerialize = is_valid([](auto&& x) -> decltype(x.serialize()) { });
如果走丢了,建议看下原文。翻译的太渣了(翻译的渣,主要是因为自己还弄不太明白SFINAE)。
看看最后成果吧:
// Notice how I simply swapped the return type on the right?template <class T> auto serialize(T& obj) -> typename std::enable_if<decltype(hasSerialize(obj))::value, std::string>::type{ return obj.serialize();}template <class T> auto serialize(T& obj) -> typename std::enable_if<!decltype(hasSerialize(obj))::value, std::string>::type{ return to_string(obj);}
FINALLY!!!
Hey, hey! Don't close this article so fast! If you are true a warrior, you can read the last part!
喂,喂!不要急着走啊!真的猛士是会看原文的!(翻译时,删掉很多不会翻译的,原文后面很有好几段)
- 深入浅出SFINAE
- 深入浅出SFINAE
- SFINAE
- SFINAE
- SFINAE使用
- result_of SFINAE
- SFINAE应用一
- SFINAE应用二
- SFINAE应用三
- SFINAE 极简介绍
- C++中的SFINAE
- C++中的SFINAE
- c++模板之SFINAE
- boost::enable_if与SFINAE原则
- C++惯用法之SFINAE
- C++模板之SFINAE技术
- 由is_base_of看C++中的SFINAE
- C++ 重载决议overload resolution 与 SFINAE
- GreenDAO,一个开源的Android ORM
- 物体检测:PVANET
- 实现B=A+A’
- ReactNative页面跳转Navigator
- 欢迎使用CSDN-markdown编辑器
- 深入浅出SFINAE
- Java面试宝典(三)
- Android开发中获取尺寸(View和屏幕分辨率)
- 第一次使用Android Studio时你应该知道的一切配置
- LLVM代码研读(1)---- 安装篇
- Unity中为UGUI精灵自定义事件响应区域
- Rails中scopes的几种使用方法
- 教你Windows平台安装配置Hadoop2.5.2(不借助cygwin)
- ContentProvider实现数据共享