美文网首页C++2.0程序猿
C++11泛型-函数模板

C++11泛型-函数模板

作者: 许了 | 来源:发表于2018-12-23 02:55 被阅读2次

    一、为什么要有函数模板

    在泛型编程出现前,我们要实现一个swap函数得这样写:

    void swap(int &a, int &b) {
        int tmp{a};
        a = b;
        b = tmp;
    }
    

    但这个函数只支持int型的变量交换,如果我们要做float, long, double, std::string等等类型的交换时,只能不断加入新的重载函数。这样做不但代码冗余,容易出错,还不易维护。C++函数模板有效解决了这个问题。函数模板摆脱了类型的限制,提供了通用的处理过程,极大提升了代码的重用性。

    二、什么是函数模板

    cppreference中给出的定义是"函数模板定义一族函数",怎么理解呢?我们先来看一段简单的代码

    #include <iostream>
    
    template<typename T>
    void swap(T &a, T &b) {
        T tmp{a};
        a = b;
        b = tmp;
    }
    
    int main() {
        int a = 2, b = 3;
        swap(a, b);  // 使用函数模板
        std::cout << "a=" << a << ", b=" << b << std::endl;
    }
    

    swap支持多种类型的通用交换逻辑。它跟普通C++函数的区别在于其函数声明(declaration)前面加了个template<typename T>,这句话告诉编译器,swap中(函数参数、返回值、函数体中)出现类型T时,不要报错,T是一个通用类型。
    函数模板的格式:

    template<parameter-list> function-declaration
    

    parameter-list是由英文逗号(,)分隔的列表,每项可以是下列之一:

    序号 名称 说明
    1 非类型形参 已知的数据类型,如整数、指针等,C++11中有三种形式:
    int N
    int N = 1: 带默认值
    int ...N: 模板参数包(可变参数模板)
    2 类型形参 swap值用的形式,格式为:
    typename|class name[ = default]
    或 typename|class ... name: 模板参数包
    3 模板模板形参 没错有两个"模板",这个比较复杂,有兴趣的同学可以参考
    cppreference之模板形参与模板实参

    上面swap函数模板,使用了类型形参。函数模板就像是一种契约,任何满足该契约的类型都可以做为模板实参。而契约就是函数实现中,模板实参需要支持的各种操作。上面swap中T需要满足的契约为:支持拷贝构造和赋值。

    template<typename T>
    void swap(T &a, T &b) {
        T tmp{a};  // 契约一:T需要支持拷贝构造
        a = b;     // 契约二:T需要支持赋值操作
        b = tmp;
    }
    

    三、函数模板不是函数

    刚才我们提到函数模板用来定义一族函数,而不是一个函数。C++是一种强类型的语言,在不知道T的具体类型前,无法确定swap需要占用的栈大小(参数栈,局部变量),同时也不知道函数体中T的各种操作如何实现,无法生成具体的函数。只有当用具体类型去替换T时,才会生成具体函数,该过程叫做函数模板的实例化。当在main函数中调用swap(a,b)时,编译器推断出此时Tint,然后编译器会生成int版的swap函数供调用。所以相较普通函数,函数模板多了生成具体函数这一步。如果我们只是编写了函数模板,但不在任何地方使用它(也不显式实例化),则编译器不会为该函数模板生成任何代码。

    函数模板实例化

    函数模板实例化分为隐式实例化和显式实例化。

    3.1 隐式实例化

    仍以swap为例,我们在main中调用swap(a,b)时,就发生了隐式实例化。当函数模板被调用,且在之前没有显式实例化时,即发生函数模板的隐式实例化。如果模板实参能从调用的语境中推导,则不需要提供。

    #include <iostream>
    
    template<typename T>
    void print(const T &r) {
        std::cout << r << std::endl;
    }
    int main() {
        // 隐式实例化print<int>(int)
        print(1);
        // 实例化print<char>(char)
        print<>('c');
        // 仍然是隐式实例化,我们希望编译器生成print<double>(double)
        print<double>(1);
    }
    

    3.2 显式实例化

    函数模板定义后,我们可以通过显式实例化的方式告诉编译器生成指定实参的函数。显式实例化声明会阻止隐式实例化。

    template<typename R, typename T1, typename T2>
    R add(T1 a, T2 b) {
        return static_cast<R>(a + b);
    }
    // 显式实例化
    template double add<double, int, double>(int, double);
    // 显式实例化, 推导出第三个模板实参
    template int add<int, int>(int, int);
    // 全部由编译器推导
    template double add(double, double);
    

    如果我们在显式实例化时,只指定部分模板实参,则指定顺序必须自左至右依次指定,不能越过前参模板形参,直接指定后面的。

    函数模板显式实例化

    四、函数模板的使用

    4.1 使用非类型形参

    #include <iostream>
    
    template<typename T, int N>
    void printArray(const T (&a)[N]) {
        std::cout << "[";
        const char *sep = "";
        for (int i = 0; i < N; i++, (sep = ", ")) {
            std::cout << sep << a[i];
        }
        std::cout << "]" << std::endl;
    }
    
    int main() {
        // T: int, N: 3
        printArray({1, 2, 3});
    }
    //输出:[1, 2, 3]
    

    4.2 返回值为auto

    有些时候我们会碰到这样一种情况,函数的返回值类型取决于函数参数某种运算后的类型。对于这种情况可以采用auto关键字作为返回值占位符。

    template<typename T1, typename T2>
    auto multi(T a, T b) -> decltype(a * b) {
        return a * b;
    }
    

    decltype操作符用于查询表达式的数据类型,也是C++11标准引入的新的运算符,其目的是解决泛型编程中有些类型由模板参数决定,而难以表示的问题。为何要将返回值后置呢?

    // 这样是编译不过去的,因为decltype(a*b)中,a和b还未声明,编译器不知道a和b是什么。
    template<typename T1, typename T2>
    decltype(a*b) multi(T a, T b) {
        return a*+ b;
    }
    //编译时会产生如下错误:error: use of undeclared identifier 'a'
    

    4.3 类成员函数模板

    函数模板可以做为类的成员函数。

    #include <iostream>
    
    class object {
    public:
        template<typename T>
        void print(const char *name, const T &v) {
            std::cout << name << ": " << v << std::endl;
        }
    };
    
    int main() {
        object o;
        o.print("name", "Crystal");
        o.print("age", 18);
    }
    

    输出:

    name: Crystal
    age: 18
    

    需要注意的是:虚函数不可以是函数模板。这是因为C++编译器在解析类的时候就要确定虚函数表(vtable)的大小,如果允许一个虚函数是函数模板,那么编译器就需要在解析这个类之前扫描所有的代码,找出这个模板成员函数的调用或显式实例化操作,然后才能确定虚函数表的大小,而显然这是不可行的。

    4.4 变参函数模板(模板参数包)

    4.5 函数模板特化

    五、其它

    5.1 函数模板 .vs. 模板函数

    函数模板重点在模板。表示这是一个模板,用来生成函数。

    模板函数重点在函数。表示的是由一个模板生成而来的函数。

    5.2 cv限定

    相关文章

      网友评论

        本文标题:C++11泛型-函数模板

        本文链接:https://www.haomeiwen.com/subject/qurgkqtx.html