深入浅出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 resolutionSFINAE 和 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 INot AError的缩略语。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修饰了某个表达式,那么就表示该表达式可以在编译期就计算出来。来个阶乘函数的栗子:


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!

喂,喂!不要急着走啊!真的猛士是会看原文的!(翻译时,删掉很多不会翻译的,原文后面很有好几段)


0 0