《C++那些事》之SFINAE

介绍c++的SFINAE概念:类成员的编译时内省

0.导语1.C++自省?2.老式的C++98方式2.1重载决议2.2 SFINAE2.3 sizeof运算符2.4 结合一切2.5 实现我们的想法2.6 小结3.C++11方式3.1 decltype, declval, auto & co4.融合时间4.1 第一种解决方案4.2 第二种解决方案5.C++14的优势5.2 auto与lambda5.2 重建is_valid6.C++177.For the fun8.总结

0.导语

本篇文章翻译自

https://jguegant.github.io/blogs/tech/sfinae-introduction.html

文中代码见《C++那些事》:

https://github.com/Light-City/CPlusPlusThings

期待您的star!

1.C++自省?

《C++那些事》之SFINAE_第1张图片 《C++那些事》之SFINAE_第2张图片

在解释什么是SFINAE之前,让我们探讨其主要用法之一:自省(introspection)。您可能已经知道,在运行时检查对象的类型或属性时,C ++并不出色。默认情况下提供的最佳功能是RTTI(Run-time type information)。不仅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.

如您所见,在序列化过程中,很容易检查对象是否具有属性并查询该属性的类型。在我们的例子中,它允许我们使用serialize方法(如果可用),否则返回到更通用的方法str。功能强大,不是吗?好吧,我们可以用纯C ++做到这一点!

这是Boost.Hana文档中使用is_valid提到的C ++ 14解决方案:

#include 
#include 
#include 

using namespace std;

namespace hana = boost::hana;
// 检查类型是否有一个serialize方法
auto hasSerialize = hana::is_valid([](auto &&x) -> decltype(x.serialize()) {});

// 序列化任意对象
template
std::string serialize(T const &obj) {
    return hana::if_(hasSerialize(obj),
                     [](auto &x) { return x.serialize(); },
                     [](auto &x) { return to_string(x); }
    )(obj);
}

// 类型A只有to_string 方法
struct A {
};

std::string to_string(const A &) {
    return "I am A";
}

// 类型B有serialize方法
struct B {
    std::string serialize() const {
        return "I am B";
    }
};

// 类型C有个serialize数据成员与to_string方法
struct C {
    std::string serialize;
};

std::string to_string(const C &) {
    return "I am C";
}

int main() {
    A a;
    B b;
    C c;
    std::cout << serialize(a) << endl;
    std::cout << serialize(b) << endl;
    std::cout << serialize(c) << endl;
}

如您所见,与Python相比,它只需要比Python多一点的样板,而不像您期望的那样复杂。它是如何工作的?好吧,如果您懒于阅读其余内容,这是我能给您的最简单的答案:与动态类型的语言不同,您的编译器一旦启动便可以访问许多静态类型信息。我们可以限制您的编译器对这些类型进行一些工作是有意义的!您想到的下一个问题是“如何?”。好吧,在下面,我们将探索各种选项,我们必须奴役我们喜欢的编译器以获取乐趣和收益!最后,我们将重新创建自己的is_valid

2.老式的C++98方式

《C++那些事》之SFINAE_第3张图片

不管你的编译器是过时的,还是你的老板拒绝为最新的Visual Studio许可证付费,或者你只是喜欢考古学,这一章都会让你感兴趣。对于那些卡在c++ 11和c++ 14之间的人来说,这也很有趣。

c++ 98中的解决方案依赖于3个关键概念:重载解析SFINAEsizeof的静态行为。

overload resolution, SFINAE and the static behavior of sizeof

2.1重载决议

当一个函数名称和某个函数模板名称匹配时,重载决议过程大致如下:

  • 根据名称找出所有适用的函数和函数模板对于适用的函数模板,要根据实际情况对模板形参进行替换;

  • 替换过程中如果发生错误,这个模板会被丢弃

  • 在上面两步生成的可行函数集合中,编译器会寻找一个最佳匹配,产生对该函数的调用

  • 如果没有找到最佳匹配,或者找到多个匹配程度相当的函数,则编译器需要报错。

一个简单的函数调用,如“f(obj);”在c++中,激活一个机制,根据参数obj来确定应该调用哪个f函数。如果一组函数可以接受obj作为参数,那么编译器必须选择最合适的函数,或者换句话说,解决最好的重载!下面是一个很好的cppreference页面,它解释了整个过程:重载解析。

https://en.cppreference.com/w/cpp/language/overload_resolution

在这种情况下,经验法则是编译器选择参数与参数最匹配的候选函数。没有什么比一个好的例子更好的了:

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 functions)。首先,函数模板接受任何类型的参数(假设是T),但是编译器的真正黑洞、魔鬼变量真空、被遗忘类型的遗忘都是可变参数函数。是的,就像可怕的C printf

可变参数函数

https://en.cppreference.com/w/cpp/utility/variadic

std::string f(...); // Variadic functions are so "untyped" that...
template  std::string f(const T& t); // ...this templated function got the precedence!

f(1); // Call the templated function version of f.

必须记住的一点是,函数模板不如可变参数函数通用。

注意:模板化函数实际上可以比普通函数更精确。但是,在平局的情况下,普通函数将具有优先级。

2.2 SFINAE

《C++那些事》之SFINAE_第4张图片

回忆一下上述的重载决议

函数调用

《C++那些事》之SFINAE_第5张图片

函数模板

《C++那些事》之SFINAE_第6张图片

SFINAE

《C++那些事》之SFINAE_第7张图片

我已经用几个段落的强大功能来戏弄你了,现在终于可以解释这个并不复杂的缩写词了。SFINAE表示替换失败不是错误( Substitution Failure Is Not An Error)。简单地说,替换就是尝试用提供的类型或值替换模板参数的机制。在某些情况下,如果替换导致无效代码,编译器不应该抛出大量错误,而应该继续尝试其他可用的重载。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  void f(const T& t, typename T::iterator* it = nullptr) { }

// The sink-hole.
void f(...) { }

f(1); // Calls void f(...) { }

上述例子中:编译器尝试f重载,因为模板化函数比可变参数函数更精确(通用)。T将被int取代,这将使我们得到void f(const int& t, int::iterator* b = nullptr); int 没有迭代器子类型,但是编译器不会抛出一堆错误。它只是尝试下一个重载。

再来回顾一下上述的简单理解:替换就是尝试用提供的类型或值替换模板参数的机制。在某些情况下,如果替换导致无效代码,编译器不应该抛出大量错误,而应该继续尝试其他可用的重载。SFINAE概念只是为“健全”的编译器保证这种“健全”的行为

所有的表达式都不会导致SFINAE。一个广泛的规则是说功能/方法主体之外的所有替代都是“安全的”。要获得更好的列表,请查看此Wiki页面。

https://en.cppreference.com/w/cpp/language/sfinae

例如,函数体内的错误替换将导致可怕的C ++模板错误

// The compiler will be really unhappy when it will later discover the call to hahahaICrash.
// 当以后发现对hahahaICrash的调用时,编译器将非常不满意。
template  void f(T t) { t.hahahaICrash(); }
void f(...) { } // The sink-hole wasn't even considered.

int main() {
    f(1);
}

经过上述的探讨,我们可以得到:

《C++那些事》之SFINAE_第8张图片

可惜has_type_x不是编译时,因此我们需要一个在编译时可确定的bool,引出sizeof运算符。

2.3 sizeof运算符

sizeof运算符确实是一个不错的工具!它允许我们在编译时返回类型或表达式的字节大小。sizeof非常有趣,因为它精确地计算表达式,就像编译表达式一样精确。例如,一个人可以做:

#include 

typedef char type_test[42];

type_test &f() {}

int main() {

    // 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() {}

int main() {
    std::cout << sizeof(f1()) << std::endl;
    std::cout << sizeof(f2()) << std::endl;
    std::cout << (sizeof(f1()) == sizeof(f2())) << std::endl; // Output 0.
}
《C++那些事》之SFINAE_第9张图片

可以看到,此时has_type_x可以在编译时计算出来对应的value。

2.4 结合一切

现在,我们有了所有工具来创建解决方案,以在编译时检查类型中方法的存在。您甚至可能已经自己解决了大部分问题。因此,让我们创建它:

《C++那些事》之SFINAE_第10张图片
#include 
#include "structData.h"
template
struct hasSerialize {
    // 编译时比较
    typedef char yes[1];
    typedef char no[2];
    // 允许我们检查序列化确实是一种方法
    // 第二个参数必须是第一个参数的类型
    // 例如:reallyHas 替换为 reallyHas 并起作用
    // 注意:它仅适用于整数常量和指针(因此函数指针可以使用)
    // 例如:reallyHas 替换为
    // reallyHas 并起作用
    template
    struct reallyHas;

    // std::string (C::*)() 是函数指针声明
    template
    static yes &test(reallyHas * /*unused*/) {}

    //  std::string (C::*)()const 函数指针 -> std::string serialize() const
    template
    static yes &test(reallyHas * /*unused*/) {}

    // The famous C++ sink-hole.
    // Note that sink-hole must be templated too as we are testing test(0).
    // If the method serialize isn't available, we will end up in this method.
    template
    static no &test(...) { /* dark matter */ }

    //用作测试的返回值的常数。
    //由于编译时评估的大小,因此实际上在这里完成了测试。
    static const bool value = sizeof(test(0)) == sizeof(yes);
    // 或者
    // enum { value = sizeof(test(0)) == sizeof(yes) };
};

int main() {
    // 检测结构体是否有serialize方法
    // Using the struct A, B, C defined in the previous hasSerialize example.
    std::cout << hasSerialize::value << std::endl;
    std::cout << hasSerialize::value << std::endl;
    std::cout << hasSerialize::value << std::endl;
}

realHas结构体有点棘手,但必须确保序列化是方法而不是类型的简单成员。您可以使用此解决方案的变体对类型进行大量测试(测试成员,子类型...),我建议您更多地搜索SFINAE技巧。

注意:如果您确实想要一个纯编译时常量,并且避免在旧编译器上出现一些错误,则可以用以下方法替换最后一个值评估:“enum { value = sizeof(test(0)) == sizeof(yes) }” 。

您可能还想知道为什么它不能与继承一起使用。C ++中的继承动态多态性是一个在运行时可用的概念,换句话说,就是编译器将不会拥有且无法猜测的数据!但是,编译时类型检查效率更高(运行时影响为0),几乎与运行时一样强大。例如:

// Using the previous A struct and hasSerialize helper.

struct D : A
{
    std::string serialize() const
    {
        return "I am a D!";
    }
};

template  bool testHasSerialize(const T& /*t*/) { return hasSerialize::value; }

D d;
A& a = d; // Here we lost the type of d at compile time.
std::cout << testHasSerialize(d) << std::endl; // Output 1.
std::cout << testHasSerialize(a) << std::endl; // Output 0.

最后但并非最不重要的是,我们的测试覆盖了主要的情况,而不是像函子那样棘手的情况:(没有考虑仿函数的情况)

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.

2.5 实现我们的想法

《C++那些事》之SFINAE_第11张图片

现在,您可能认为使用我们的hasSerialize来创建一个序列化函数非常容易!好吧,我们试试

template  std::string serialize(const T& obj)
{
    if (hasSerialize::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函数,但是却调用了,当然出错了。

您的编译器确实是个好人,不会遗忘任何分支,因此在这种情况下,obj必须同时具有serialize方法和to_string重载。解决方案包括将序列化功能分为两个不同的功能:一个仅使用obj.serialize(),另一个根据obj的类型使用to_string

我们回到一个已经解决的较早的问题,如何根据类型拆分?SFINAE,可以肯定!到那时,我们可以将hasSerialize函数重新构造为序列化函数,并使其返回std :: string而不是编译时boolean。但是我们不会那样做!将hasSerialize测试与其使用序列化分开是比较干净的。

这个问题如何解决呢?

第一种解决方案:加上constexpr,具体后面阐述。

C++17 引入 if constexpr 支持在编译期执行, 可以将之应用于泛型编程中的条件判断,

if constexpr (hasSerialize::value)

第二种解决方案:就是不用if语句了,而是将这个函数分成两个函数,每个函数对应一个分支。如何分?用enable_if:

《C++那些事》之SFINAE_第12张图片 SFINAE-14
template // Default template version.
struct enable_if {}; // This struct doesn't define "type" and the substitution will fail if you try to access it.

template // A specialisation used if the expression is true.
struct enable_if { typedef T type; }; // This struct do have a "type" and won't fail on access.

// Usage:
enable_if::type t1; // Compiler happy. t's type is int.
enable_if::value, int>::type t2; // Compiler happy. t's type is int.

enable_if::type t3; // Compiler unhappy. no type named 'type' in 'enable_if';
enable_if::value, int>::type t4; // no type named 'type' in 'enable_if';

我们需要在“template std::string serialize(const T& obj)”签名上找到一个巧妙的SFINAE解决方案。我带给您难题的最后一部分,称为enable_if。

如您所见,我们可以使用enable if根据编译时表达式触发替换失败。现在我们可以在“template std::string serialize(const T& obj)”签名上使用这个错误来调度到正确的版本。最后,我们找到了问题的真正解决办法:

《C++那些事》之SFINAE_第13张图片
template  typename enable_if::value, std::string>::type serialize(const T& obj)
{
    return obj.serialize();
}

template  typename enable_if::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;

值得注意两个细节!

首先,我们在返回类型上使用enable_if,以保持参数推导,否则我们将必须明确指定类型"serialize(a)"。

其次,即使使用to_string的版本也必须使用enable_if,否则serialize(b)将有两个潜在的可用重载并引起歧义。如果您想查看此C ++ 98版本的完整代码,这里是要点。在C ++ 11中,生活要容易得多,所以让我们看一下这个新标准的美妙之处!

注意:同样重要的是要知道这段代码在一个表达式上创建了一个SFINAE(“&C::serialize”)。虽然这个特性不是c++ 98标准所要求的,但是它已经在使用了,这取决于您的编译器。它确实成为c++ 11中一个安全的选择。

2.6 小结

以上C++98方式总结说出下面问题:

《C++那些事》之SFINAE_第14张图片 SFINAE-14

3.C++11方式

《C++那些事》之SFINAE_第15张图片 SFINAE-14

在2000年的大世纪闰年之后,人们对未来几年相当乐观。有些人甚至决定为像我这样的下一代c++程序员设计一个新的标准!这个标准不仅可以减轻TMP的麻烦(模板元编程的副作用),而且在第一个十年就可以使用,因此它的代码名为c++ 0x。好吧,这个标准不幸地出现在下一个十年(2011 ==> C ++ 11),但是它带来了许多有趣的特性。让我们回顾一下他们。

3.1 decltype, declval, auto & co

decltype

《C++那些事》之SFINAE_第16张图片

还记得sizeof操作符对传递给它的表达式进行“伪计算”,然后返回表达式类型的大小吗?

c++ 11增加了一个新的运算符decltypedecltype给出了它要计算的表达式的类型。由于我的善良,我不会让你举一个例子,直接给你:

B b;
decltype(b.serialize()) test = "test"; // Evaluate b.serialize(), which is typed as std::string.
// Equivalent to std::string test = "test";

declval是一个实用程序,可为您提供对无法轻松构造的类型的对象的“伪引用”。declval对于我们的SFINAE结构确实非常方便。cppreference示例非常简单,因此这里是一个副本:

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().foo()) n2 = n1; // int n2
    std::cout << "n2 = " << n2 << '\n';
}

auto

《C++那些事》之SFINAE_第17张图片

auto specifier 指定被声明的变量的类型将被自动推导。auto相当于c#中的var。auto在c++ 11中也有一个不太出名的函数声明用法。这里有一个很好的例子:

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  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  auto g(const T& t) -> decltype(t.serialize()) {   } // No compilation error.

如您所见,auto允许使用尾随返回类型语法,并使用decltype以及涉及函数参数之一的表达式。这是否意味着我们可以使用它来测试SFINAE序列化的存在?

是的,沃森博士!decltype很快就会亮起来,您必须等待C ++ 14才能完成这种棘手的自动用法(但是由于它是C ++ 11的功能,因此最终会在这里出现)。

constexpr

《C++那些事》之SFINAE_第18张图片

c++ 11还提供了一种执行编译时计算的新方法!

新的关键字constexpr是编译器的一个提示,这意味着这个表达式是常量,可以在编译时直接求值。在c++ 11中,constexpr有很多规则,只能使用一小部VIEs(非常重要的表达式)表达式(没有循环……)!我们仍然有足够的时间来创建编译时阶乘函数:

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;

constexpr增加了STL中std :: true_typestd :: false_type的使用。顾名思义,这些类型封装了constexpr布尔值“ true”和constrexpr布尔值“ false”。它们最重要的属性是类或结构可以从它们继承。例如:

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;

4.融合时间

《C++那些事》之SFINAE_第19张图片

4.1 第一种解决方案

在烹饪中,一个好的食谱需要把所有最好的配料按正确的比例混合。如果您不想在晚餐时使用1998年的意大利面条代码,那么让我们重新访问2011年的c++ 98 hasSerializeserialize函数,其中包含“新鲜”的成分。让我们从消除腐烂的方法开始,使用美味的decltype和bake 一点点的constexpr而不是sizeof。在烤箱中烘烤15分钟后(或出现新的头痛症状),您将获得:

《C++那些事》之SFINAE_第20张图片
template
struct hasSerialize {
    // We test if the type has serialize using decltype and declval.
    template
    static constexpr decltype(std::declval().serialize(), bool()) test(int /* unused */) {
        // We can return values, thanks to constexpr instead of playing with sizeof.
        return true;
    }

    template
    static constexpr bool test(...) {
        return false;
    }

    // int is used to give the precedence!
    static constexpr bool value = test(int());
};

您可能对我使用decltype感到有些困惑。C ++逗号运算符“,”可以创建多个表达式链。在decltype中,将评估所有表达式,但仅将最后一个表达式视为该类型。序列化不需要任何更改,减去了STL中现在提供了enable_if函数的事实。

测试如下:

template
std::string serialize(const T &obj) {
    // 不加constexpr 报错:error: no member named 'serialize' in 'A'.
    // C++17的constexpr
    if constexpr (hasSerialize::value)
        return obj.serialize();
    else
        return to_string(obj);
}

int main() {
    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;
}

4.2 第二种解决方案

《C++那些事》之SFINAE_第21张图片

Boost.Hanna文档中介绍的另一个使用std :: true_typestd :: false_type的C ++ 11解决方案是这样的:

template
struct hasSerialize : std::false_type {

};
template
struct hasSerialize().serialize())> : std::true_type {

};
// 测试同上

我个人认为,这种解决方案更加狡猾!它依赖于不太知名的默认模板参数。但是,如果您的灵魂已经(堆栈)损坏,您可能会意识到默认参数会在专业领域传播。因此,当我们使用hasSerialize :: value时,默认参数会起作用,并且实际上我们在 primary templatespecialisation方面都正在寻找hasSerialize :: value。同时,将处理decltype的替换和求值,并且如果OurType具有返回std :: string的序列化方法,则我们的specialisation会被替换为具有签名hasSerialize ,否则替换将失败。因此,在良好情况下,specialisation优先。在这种情况下,将可以使用std :: void_t C ++ 17帮助程序。无论如何,这是您可以使用的要点!

我告诉过你,第二种解决方案隐藏了很多复杂性,我们仍然有很多c++ 11特性没有被利用,比如nullptr、lambda、r-values。不用担心,我们将在c++ 14中使用其中的一些。

5.C++14的优势

5.2 auto与lambda

根据我的XFCE环境右上角的公历,我们是2015年!我可以安全地在我最喜欢的编译器上打开C ++ 14编译标志,不是吗?好吧,我可以使用clangMSVC是否使用maya日历?)。再一次,让我们探索新功能,并使用它们来构建精彩的东西!就像我在本文开头所承诺的那样,我们甚至将重新创建一个is_valid

auto

(1)返回类型推断的结果

c++ 14中的一些很酷的特性来自于auto关键字的轻松使用(用于类型推断的关键字)。现在,auto可以用于函数或方法的返回类型。例如:

auto myFunction() // Automagically figures out that myFunction returns ints.
{
    return int();
}

只要类型很容易被编译器“猜测”,它就可以工作。毕竟我们是在用c++编程,而不是OCaml

lambda

(2)函数爱好者的功能

《C++那些事》之SFINAE_第22张图片

c++ 11介绍了lambdalambda具有以下语法:

[capture-list](params) -> non-mandatory-return-type { ...body... }

在我们的例子中,一个有用的例子是:

int main() {
    B b;
    auto l1 = [](B &b) { return b.serialize(); }; // Return type figured-out by the return statement.
    auto l3 = [](B &b) -> std::string { return b.serialize(); }; // Fixed return type.
    auto l2 = [](B &b) -> decltype(b.serialize()) { return b.serialize(); }; // Return type dependant to the B type.

    std::cout << l1(b) << std::endl; // Output: I am a B!
    std::cout << l2(b) << std::endl; // Output: I am a B!
    std::cout << l3(b) << std::endl; // Output: I am a B!
}

c++ 14lambdas带来了一个小的变化,但是带来了巨大的影响!

Lambdas接受自动参数:根据参数推导出参数类型。Lambdas被实现为一个具有新创建的未命名类型(也称为闭包类型)的对象。如果一个lambda有一些自动参数,它的“函子操作符”操作符()将被简单地模板化。让我们来看看:

void fun(A a,B b,C c) {
    // ***** 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'.
    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'.
}

除了lambda本身,我们对生成的未命名类型更感兴趣:它的lambda操作符()可以用作SFINAE!

正如您所看到的,编写lambda比编写等价类型要简单。这应该使你想起我最初的解决办法:

// Check if a type has a serialize method.
auto hasSerialize = hana::is_valid([](auto&& x) -> decltype(x.serialize()) { });

好消息是,我们现在可以重新创建is_valid一切!

5.2 重建is_valid

《C++那些事》之SFINAE_第23张图片

现在,我们已经有了一种非常时尚的方式,可以使用lambda生成具有潜在SFINAE属性的未命名类型,我们需要弄清楚如何使用它们!如您所见,hana :: is_valid是一个将lambda作为参数并返回类型的函数。我们将is_valid返回的类型称为containercontainer将负责保留lambda的未命名类型以供以后使用。让我们从编写is_valid函数及其container开始:

《C++那些事》之SFINAE_第24张图片
template  struct container
{
    // Remembers UnnamedType.
};

template  constexpr auto is_valid(const UnnamedType& t) 
{
    // We used auto for the return type: it will be deduced here.
    return container();
}

auto test = is_valid([](const auto& t) -> decltype(t.serialize()) {})
// Now 'test' remembers the type of the lambda and the signature of its operator()!

下一步是使用operator操作符()扩展容器,例如我们可以用一个参数调用它。此参数类型将针对UnnamedType进行测试!为了对参数类型进行测试,我们可以再次对一个重新创建的'UnnamedType'对象使用SFINAE !它给出了这个解决方案:

《C++那些事》之SFINAE_第25张图片
template  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  constexpr auto testValidity(int /* unused */)
    -> decltype(std::declval()(std::declval()), std::true_type())
    {
        // If substitution didn't fail, we can return a true_type.
        return std::true_type();
    }

    template  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  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(int());
    }
};

template  constexpr auto is_valid(const UnnamedType& t) 
{
    // We used auto for the return type: it will be deduced here.
    return container();
}

// Check if a type has a serialize method.
auto hasSerialize = is_valid([](auto&& x) -> decltype(x.serialize()) { });

如果您在这一点上有点困惑,我建议您花点时间重新阅读前面的所有示例。你已经拥有了所有你需要的武器,现在开始与c++战斗吧!

最后! ! !我们有一个工作是有效的,我们可以使用它的序列化!

如果我和我的SFINAE技巧一样邪恶,我会让你复制每个代码片段来重新创建一个完整的工作解决方案。但今天,万圣节的精神与我同在,这里是要点。嘿,嘿!不要这么快就结束这篇文章!如果你是真正的战士,你可以读最后一部分!

6.C++17

《C++那些事》之SFINAE_第26张图片

前面已经使用过这个方法了,这里提及一下即可。

7.For the fun

我没有告诉你几件事,是故意的。否则,我担心这篇文章要长两倍。我强烈建议您向Google询问有关我要说的内容的更多信息。

(1)首先,如果您希望有一个与Boost一起工作的解决方案。Boost.Hana static if_,您需要通过Hana的等效物来改变testValidity方法的返回类型,如下所示:

template  constexpr auto test_validity(int /* unused */)
-> decltype(std::declval()(std::declval()), boost::hana::true_c)
{
    // If substitution didn't fail, we can return a true_type.
    return boost::hana::true_c;
}

template  constexpr decltype(boost::hana::false_c) test_validity(...)
{
    // Our sink-hole returns a false_type.
    return boost::hana::false_c;
}

静态if实现非常有趣,但至少与我们在本文中解决的问题一样困难。也许有一天,我会再写一篇关于它的文章

(2)如果您注意到我们一次只检查一个参数?我们不能做这样的事情:

auto test = is_valid([](auto&& a, auto&& b) -> decltype(a.serialize(), b.serialize()) { });
A a;
B b;

std::cout << test(a, b) << std::endl;

实际上,我们可以使用一些参数包。这是解决方案:

template  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  constexpr auto test_validity(int /* unused */)
    -> decltype(std::declval()(std::declval()...), std::true_type())
    {
        // If substitution didn't fail, we can return a true_type.
        return std::true_type();
    }

    template  constexpr std::false_type test_validity(...)
    {
        // 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  constexpr auto operator()(Params&& ...)
    {
        // The argument is forwarded to one of the two overloads.
        // The SFINAE on the 'true_type' will come into play to dispatch.
        return test_validity(int());
    }
};

template  constexpr auto is_valid(UnnamedType&& t)
{
    // We used auto for the return type: it will be deduced here.
    return container();
}

// Check if a type has a serialize method.
auto hasSerialize = is_valid([](auto &&x) -> decltype(x.serialize()) {});

// Notice how I simply swapped the return type on the right?
template
auto serialize(T &obj)
-> typename std::enable_if::type {
    return obj.serialize();
}

template
auto serialize(T &obj)
-> typename std::enable_if::type {
    return to_string(obj);
}

int main() {
    A a;
    B b;
    C c;
    auto test = is_valid([](const auto &t) -> decltype(t.serialize()) {});

    std::cout << test(a,b) << std::endl;
    std::cout << test(b) << std::endl;
    std::cout << test(c) << std::endl;
    // 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;
}

8.总结

《C++那些事》之SFINAE_第27张图片

《C++那些事》之SFINAE_第28张图片

你可能感兴趣的:(c++,python,开发语言)