深入浅出SFINAE

深入浅出SFINAE


翻译的很差,最好去看原文。

原文链接: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!

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

  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: PCS7是西门子公司生产的一种工业自动化控制系统,是一种功能强大且易于使用的平台,用于控制和监控工业过程中的各种设备和系统。PCS7深入浅出 V8是PCS7的一个版本,它在之前的版本基础上进行了技术升级和功能扩展。 PCS7深入浅出 V8拥有更加直观和用户友好的界面,使操作更加简单易学。它采用了全新的图形组织和设计方式,使得用户可以更轻松地理解和使用系统。同时,它还提供了更多的可定制性,可以根据用户的需求进行个性化配置和设置。 在功能方面,PCS7深入浅出 V8具有更高的可靠性和稳定性。它采用了分布式架构,可以同时控制多个设备和系统,实现更加复杂的工艺过程控制。同时,它还具备故障诊断和报警功能,可以及时发现和解决问题,确保工业生产的连续性和稳定性。 PCS7深入浅出 V8还拥有更强大的数据处理和分析能力。它可以实时采集和存储大量的工业数据,并通过先进的算法和模型进行分析和预测。这样,用户可以根据数据的分析结果进行调整和优化,提高生产效率和质量。 总的来说,PCS7深入浅出 V8是一款功能强大且易于使用的工业自动化控制系统。通过它,用户可以更加轻松地控制和监控工业过程中的各种设备和系统,实现更高效、更可靠的工业生产。 ### 回答2: PCS7是西门子工业自动化控制系统中的一款重要软件,它的最新版本是V8。PCS7深入浅出 V8指的是对于PCS7 V8版本的深入理解和简单易懂的介绍。 PCS7 V8是在之前版本的基础上进行了升级和改进,以提供更高效、更稳定、更灵活的自动化控制系统。它具有以下几个重要特点: 首先,PCS7 V8在硬件和软件方面都进行了优化。在硬件方面,它支持多种不同类型的通信协议,可以与各种工业设备进行连接,实现数据的传输和控制。在软件方面,PCS7 V8增加了许多新功能和模块,包括智能监控、故障诊断、过程数据分析等,进一步提高了系统的自动化程度和控制精度。 其次,PCS7 V8具有良好的可扩展性和灵活性。它支持分布式控制架构,可以将控制任务分散到不同的控制器上,实现系统的并行运行和负载均衡。同时,PCS7 V8的功能模块和监控界面都支持自定义设置,可以根据用户的需求进行个性化定制,满足不同行业和企业的特殊需求。 再次,PCS7 V8具备强大的数据处理和分析能力。它可以实时采集和存储大量的过程数据,支持数据的查询、统计和报表生成等功能,帮助用户对生产过程进行全面监控和分析。同时,PCS7 V8还可以与企业级信息系统进行集成,实现生产数据的共享和优化,提高企业的生产效率和竞争力。 综上所述,PCS7 V8是一款功能强大、性能优越的自动化控制系统软件,它通过深入浅出的方式,为用户提供了更简单易懂的操作界面和更全面精细的控制功能。无论是在工业生产还是其他领域,PCS7 V8都能帮助用户实现自动化控制和智能化管理,提高生产效率和产品质量。 ### 回答3: PCS7是西门子(Siemens)公司推出的一款自动化系统软件,它能够广泛应用于工业自动化领域的控制系统中。PCS7深入浅出 v8 是PCS7软件的一个新版本,它在前一版本的基础上进行了一些改进和升级。 首先,PCS7深入浅出 v8在系统可靠性方面进行了改进。它采用了分布式控制技术,可以将控制任务分配给多个控制单元,提高了系统的容错性和可靠性。同时,它还具备故障自诊断功能,能够实时检测系统中的故障并给出相应的报警信息,方便工程师进行故障处理。 其次,PCS7深入浅出 v8在网络通信方面进行了优化。它支持多种网络通信协议,能够与其他设备和系统进行无缝集成。同时,它还具备远程监控和控制功能,使得工程师可以通过互联网对远程设备进行监控和控制,提高了生产效率和管理效果。 此外,PCS7深入浅出 v8还具备强大的数据处理能力。它可以实时采集、存储和分析系统中的各种数据,对生产过程进行监控和优化。同时,它还提供了丰富的数据展示和报表生成功能,方便用户进行数据分析和决策。 综上所述,PCS7深入浅出 v8是一款功能强大、性能稳定的工业自动化系统软件。它的改进和升级使得工业生产过程更加可靠、高效、智能化。通过使用PCS7深入浅出 v8,企业可以提高生产效率,降低运营成本,提高竞争力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值