C++模板 - 16(SFINAE)

C++支持函数重载,同一个函数名,只要它的签名不一样,可以声明若干个版本(这个特性也是必须的,不然构造函数就只能有一个了)。

现在函数的重载集合中又加入了新的成员 - 函数模板,事情就变得越发有趣起来,特别是还可以对这些函数模板进行完全特化。这使得编译器在匹配函数调用时的选择有时可能会出乎你的意料。

不过我们这次讲的是另外一个话题,当编译器在尝试匹配某个函数模板时,会使用调用参数的类型来推导模板参数,这个过程中编译器可能会发现这个函数模板对于这个调用参数来说生成的代码是无效的或者非法的。这个时候编译器并不会报错,而是将这个函数模板从匹配的候选列表中剔除,这个被称为“SFINAE”(Substitution Failure Is Not An Error)。

我们可以利用这个特性,定义满足不同约束的函数模板,然后可以放心地在函数体内使用这些约束,让满足各自约束的调用参数分别去匹配相应的函数模板。

下面看个例子,实现方式比较原始,但是可以很好地解释这个特性。

template
struct HasSerialize {
    template
    struct ReallyHas;

    template
    static std::true_type test(ReallyHas*) {}

    template
    static std::true_type test(ReallyHas*) {}

    template
    static std::false_type test(...) {}

    static constexpr bool value = std::is_same_v(nullptr))>;
};

这个类模板的功能是检测其模板参数T是否定义有serialize成员函数。比如说我们有一个序列化对象的框架,如果对象本身定义有serialize,这样我们就可以直接使用它。这个类模板使用了3个重载的成员函数模板test来实现检查操作(这个实现比较烦琐,其实有更为简单的实现方式,这里主要是用它来解释SFINAE)。

我们先定义了一个内嵌的类模板ReallyHas,它的第一个模板参数是个类型,第二个是非类型参数,是第一个类型的某个值。前两个test基本一样(区别在于多了一个函数的const限定符),这里的约束就是类型具有serialize成员函数,其返回值为std::string,参数为空。第三个test则负责匹配那些无法满足约束的那些类型替换。

class MySerialize {
   public:
    std::string serialize() { return std::string{}; }
};

class MySerialize2 {
   public:
    std::string serialize() const { return std::string{}; }
};

class NoSerialize {};

HasSerialize::value; // true
HasSerialize::value; // true
HasSerialize::value; // false

上面的例子可以看成是SFINAE的一个应用场景,用来检测类型的约束(是否有某个内嵌类型的定义,或者是否定义有某个特定的成员函数)。SFINAE在C++20之前被大量用于类型的约束检测,比较简便的使用方式是配合decltype,注意要转换为void防止逗号运算符被重载导致的检测失败(这里就不举例了,因为concept出现之后,这些使用技巧慢慢就成为历史了)。

下面看一个SFINAE真正应用的例子吧。

template
std::size_t len(T (&)[N]) {
    return N;
};

template
typename T::size_type len(const T& t) {
    return t.size();
};

我们定义了一个len函数,对于某个数组,则返回数组的维度,对于其他类型,则有两个约束:该类型有内嵌类型定义size_type,且有size成员函数,其返回类型为size_type。

    int a[5];
    std::cout << len(a); // 5
    std::cout << len("hello, world"); // 13
    std::vector v;
    std::cout << len(v); // 0
class Foo {};
std::cout << len(Foo{}); // no type named 'size_type' in 'Foo'

我们定义了一个Foo类,len对于Foo类对象,只有第二个len可以匹配,但是它没有size_type的类型定义,这时编译器给出了错误信息。

std::size_t len(...) {
    return 0;
}

这次我们添加了len的第三个版本,它适用于任何参数,但它是个最差的匹配(匹配度最低),可以看作是所有无法匹配的类型的收容站。这次len(Foo{})会匹配这个版本,返回值为0(这个就是SFINAE,这时第二个版本的len不会再抱怨没有size_type类型定义了)。

class Foo {
   public:
    using size_type = int;
};

后面有一天,我们修改了这个Foo的定义,添加了size_type的类型定义(但是还是没有定义size成员函数)。然后,你的程序突然就编译不过了。

std::cout << len(Foo{}); // error: no member named 'size' in 'Foo'

这次len的第二个版本会匹配成功,因为这次它的签名匹配是成功的,但是在函数体内调用size成员函数时失败了。这个时候并不会再次回到那个收容站上去,而是直接编译失败。这个区别大家需要留意:SFINAE的应用只在函数重载时的签名匹配过程中,一旦匹配成功,这个时候编译器就完成函数的重载选择了,其后的代码必须是有效的,否则不再有fallback了。

展开阅读全文

页面更新:2024-05-02

标签:模板   可能会   编译器   函数   例子   定义   成员   参数   版本   类型

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top