cpp模板

作者: _张鹏鹏_ | 来源:发表于2021-10-03 14:26 被阅读0次

    总结下c++模板相关的基础知识,便于查阅。

    模板:

    模板定义以关键字template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或者多个模板参数的列表,用小括号和大括号包围起来。

    模板是个半成品,模板编译通过但是使用错误仍然会报错;

    可以将模板看作是编译期函数;

    运行期函数的参数要为一个对象(不能是模板);编译期函数的参数可以是模板;

    声明:

    模板声明必须包含模板参数:

    // 声明但是不定义compare和Blob
    template<typename T> int compare(const T&, constT&);
    template<typename T> class Blob;
    

    与函数参数相同,声明中的模板参数的名字不必与定义中相同,但是一个给定模板的声明和定义必须有相同数量和种类的参数:

    //声明
    template<typename T> T calc(const T&, const T&);
    template<typename U> U calc(const U&, const U&);
    // 定义
    template<typename Type>
    Type calc(const Type& a, const Type& b)
    {
        /*...*/
    }
    

    以上三个calc都指向相同的函数模板。

    函数模板:

    template<typename T>
    void Swap(T&a, T&b)
    {
        T tmp;
        tmp = a;
        a = b;
    }
    

    上边就定义了一个变量交换的函数模板,在使用函数模板时有两种方式:

    int a = 10, b = 20;
    Swap(a, b)      // 自动类型推导调用
    Swap<int>(a, b) //  指定类型显式调用
    

    在调用一个函数模板时,编译器用函数实参来为我们推断模板实参,在调用Swap时,编译器使用实参的类型来确定绑定到模板参数T的类型,这里编译器会推断出模板的实参为int,并将它绑定了模板参数T。

    函数模板是不允许隐式类型转换的,调用时类型必须严格匹配

    int a = 10;
    float b = 20.5;
    Swap(a, b)      // 类型不匹配,编译器报错!
    

    参数:

    函数模板参数支持以下类型:

    1. 类型参数:使用typename或者class指定;
    2. 非类型参数:整型常量(包括enum),或者指向对象或者函数的指针或左值引用。绑定到非类型整形参数的实参必须是一个常量表达式。绑定到指针或者引用非类型参数的实参必须具有静态的生命周期。
    // 非类型参数:
    template<int a, int b>
    void test()
    {
        cout << a + b << endl;
    }
    
    template<unsigned M, unsigned N>
    int compare(const char (&p1)[M], const char (&p2)[N])
    {
        return strcmp(p1, p2);
    }
    
    test<3,4>();
    //实例化如下版本:int compare(const char (&p1)[3], const char (&p2)[4])
    compare("hi", "mom"); 
    

    重载:

    函数模板跟普通函数一样,也可以被重载:

    1. C++编译器优先考虑普通函数。
    2. 如果函数模板可以产生一个更好的匹配,那么就选择函数模板,也可以通过空模板实参列表限定编译器只匹配函数模板。

    下面是函数模板的示例:

    void fun(int a, float b)
    {
        cout << "void fun(int a, float b)" << endl;
    }
    
    template<typename T>
    void fun(T a)
    {
        cout << "void fun(T a)" << endl;
    }
    
    template<typename T1, typename T2>
    void fun(T1 a, T2 b)
    {
        cout << "void fun(T1 a, T2 b)" << endl;
    }
    
    template<typename T1, typename T2, typename T3>
    void fun(T1 a, T2 b, T3 c)
    {
        cout << "void fun(T1 a, T2 b, T3 c)" << endl;
    }
    
    // 类似地可以将泛型T看作变量, 推导后的类型看作赋给变量的值
    int main(int argc, char* argv[])
    {
        int a = 0;
        float b = 0;
        fun(a); fun(b); // fun(T a)这里T分别被赋值为int和float
        fun(a,b);       // 优先匹配普通函数void fun(int a, float b)
        fun<>(a,b);     // 使用限定符<>时,编译器就会去匹配函数模板。
        int c = 0;
        fun(a, c);      // void fun(T1 a, T2 b) 这里T1、T2分别被赋值为int和int
        fun(a, b, c);   // void fun(T1 a, T2 b, T3 c)  这里T1、T2、T3分别被赋值为int、float和int
        return 0;
    }
    

    类模板:

    类模板是用来生成类的蓝图,与函数模板不同的是,编译器不能为类模板推断模板参数类型。为了使用类模板,需要在模板名后的尖括号中提供额外信息-----用来代替模板参数的模板实参列表。

    vector<int> ivec;
    vector<Sales_item> Sales_vec;
    vector<vector<string>> file;
    

    参数:

    模板参数支持以下类型:

    1. 类型参数:使用typename或者class指定;
    2. 模板参数:使用template<...> class XXX的形式指示。
    3. 非类型参数:整型常量(包括enum),或者指向外部链接的指针(包括函数指针、类的成员函数指针,以及具有外部链接的字符串常量指针);

    类型参数:

    //类型参数: T,Container
    // std::vector<T>是默认参数
    template<typename T, typename Container = std::vector<T>>
    struct Stack
    {
        void push(const T& elem)
        {
            elems.push_back(elem);
        }
    
        T pop()
        {
            if (empty()) throw std::out_of_range("Stack<>::pop: empty!");
    
            auto elem = elems.back();
            elems.pop_back();
            return elem;
        }
    
        bool empty() const
        {
            return elems.empty();
        }
    
    private:
        Container elems;
    };
    // 使用示例:
    Stack<int, vector<int>> intStack;
    

    我们可以将类模板想象成一个编译期的函数,不同的是它的参数列表放在一对尖括号中。通过template struct Stack我们声明了一个编译期的函数,它的名字叫做Stack,它有一个类型形参T。

    标准规定可以用typename或者class关键字指示模板形参是一个类型,不能使用struct。由于模板的类型形参不仅可以被替换为用户自定义类型,也可以被替换为内置类型(int, char, double...),所以使用typename语义上比class更清晰一些。

    模板参数:

    // 类型参数T; 模板参数Container
    /*
    按照标准这里声明Container前的关键字只能是class,不能是typename。
    */
    template<typename T, 
             template<typename Elem, typename Allocator = std::allocator<Elem>> class Container = std::vector>
    struct Stack1
    {
        void push(const T& elem)
        {
            elems.push_back(elem);
        }
    
        T pop()
        {
            if (empty()) throw std::out_of_range("Stack<>::pop: empty!");
    
            auto elem = elems.back();
            elems.pop_back();
            return elem;
        }
    
        bool empty() const
        {
            return elems.empty();
        }
    
    private:
        Container<T> elems;
    };
    
    // 使用示例:
    Stack1<int, vector> intStack;
    

    如果我们将类模板比作C++编译期的函数,那么可以接受模板作为参数的类模板,就相当于一个函数的入参仍旧可以是函数。

    非类型参数:

    template<typename T, int MAX_SIZE>
    struct Stack
    {
        void push(const T&);
        T pop();
        
    private:
        T elems[MAX_SIZE];
        int size;
    };
    
    // 使用示例:
    Stack<int, 5>  stack;
    

    如下用数组实现Stack模板,第二个模板参数是一个int型常量,用于定义数组的最大长度。

    特化:

    主模板如下:

    template<typename T,
             template<typename Elem, typename Allocator = std::allocator<Elem>> class Container = std::vector>
    struct Stack
    {
        void push(const T& elem)
        {
            elems.push_back(elem);
        }
    
        T pop()
        {
            if (empty()) throw std::out_of_range("Stack<>::pop: empty!");
    
            auto elem = elems.back();
            elems.pop_back();
            return elem;
        }
    
        bool empty() const
        {
            return elems.empty();
        }
    
    private:
        Container<T> elems;
    };
    

    主模板隐式要求模板Container需要支持push_back、back、pop_back、empty等接口;

    如果目前有一个已经存在的类模板Array,想让Array作为Stack的底层容器:

    template<typename T, typename Allocator = std::allocator<T>>
    struct Array
    {
        void put(size_t index, const T& t)
        {
            /***/
        }
        T get(size_t index)
        {
            /***/
        }
    private:
    
    };
    

    显然Array没有满足主模板的约束(提供push_back、back、pop_back、empty等接口)。这里可以使用模板特化,特化模板类的定义必须在主模板类的后面:

    template<typename T>
    struct Stack<T, Array>
    {
        Stack() : size(0)
        {
        }
    
        void push(const T& elem)
        {
            elems.put(size++, elem);
        }
    
        T pop()
        {
            if (empty()) throw std::out_of_range("Stack<>::pop: empty!");
            return elems.get(--size);
        }
    
        bool empty() const
        {
            return size == 0;
        }
    
    private:
        size_t size;
        Array<T> elems;
    };
    

    注意,主模板的template关键字后面定义了该模板的基本原型特征,特化模板的模板名称关键字后面的尖括号中的模板参数必须和主模板template关键字后面尖括号中的参数顺序和约束一致。上例中由于主模板声明第一个模板参数是类型,第二个模板参数是模板,所以特化版本Stack尖括号中的参数不能多也不能少,且顺序不能颠倒,而且第二个参数模板Array的定义必须和主模板中对Container的模板约束一致。

    特化版本的template后面紧跟的尖括号中仅是声明特化版本中还在使用的非具体类型参数,和主模板template后面紧跟的尖括号中的参数没有任何关系。

    上例中参数里还有非具体类型,那么就叫做部分特化或者偏特化,如果特化版本中,所有的模板参数都被替换成了具体类型,那么就叫做全特化,例如:

    template<> 
    struct Stack<int*, Array>
    {
        ...
    };
    

    无论是全特化还是偏特化,特化版本的声明仍然需要使用关键字template,后面紧跟的尖括号中声明特化版本中还在使用的非具体类型形参。由于全特化不再存在非具体类型,所以尖括号中为空,但是不能省略,皆以template <>开头。

    递归:

    template<int N>
    struct Sum
    {
        enum {sum = Sum<N-1>::sum + N };
    };
    
    template<>
    struct Sum<0>
    {
        enum {sum = 0 };
    };
    
    //
    Sum<7>::sum;
    
    template<int N>
    struct Factorial
    {
        enum { Value = N * Factorial<N - 1>::Value };
    };
    
    template<>
    struct Factorial<1>
    {
        enum { Value = 1 };
    };
    //
    Factorial<8>::Value;
    

    变参模板:

    可变模版参数(variadic templates)是C++11新增的最强大的特性之一。本节主要介绍模板存在可变参数时,函数模板,类模板如何展开参数包。

    函数模板:

    一个简单的可变模板参数函数:

    // 打印可变参数个数
    template<typename ...Types>
    void f(Types... args)
    {
        cout << sizeof...(args) << endl;
    }
    
    f();        //0
    f(1, 2);    //2
    f(1, 2.5, "");    //3
    

    展开可变模版参数函数的方法一般有两种:一种是通过递归函数来展开参数包,另外一种是通过逗号表达式来展开参数包。

    // 递归终止函数
    void print()
    {
        cout << "empty" << endl;
    }
    
    template<typename Head, typename ...Tail>
    void print(Head head, Tail... tail)
    {
        cout << "param:"<<head << endl;
        print(tail...);
    }
    
     print(1,2,3,4);
    

    上例会输出每一个参数,直到为空时输出empty。展开参数包的函数有两个,一个是递归函数,另外一个是递归终止函数,参数包Args...在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一个,直到所有的参数都展开为止,当没有参数时,则调用非模板函数print终止递归过程。

    递归调用的过程是这样的:

    print(1,2,3,4);
    print(2,3,4);
    print(3,4);
    print(4);
    print();
    

    上面的递归终止函数还可以写成这样:

    template <class T>
    void print(T t)
    {
       cout << t << endl;
    }
    

    修改递归终止函数后,上例中的调用过程是这样的:

    print(1,2,3,4);
    print(2,3,4);
    print(3,4);
    print(4);
    

    例子:

    下面再给几个例子:

    求和:

    template<typename T>
    T sum(T t)
    {
        return t;
    }
    
    template<typename T, typename ...Types>
    T sum(T t, Types... types)
    {
        return t + sum(types...);
    }
    // 编译会告警,double类型转int精度丢失。
    sum(1,2,3,4,5.5);
    

    打印元组tuple:

    #include <iostream>
    #include <string>
    #include <bitset>
    #include <tuple>
    using namespace std;
    
    namespace  print_tuple
    {
        using namespace std;
        template<typename ...Types>
        ostream &operator<<(ostream &os, const tuple<Types...>&t)
        {
            os << "[";
            PRINT_TUPLE<0, sizeof...(Types), Types...>::print(os, t);
            return os << "]";
        }
    
        template<int IDX, int MAX, typename ...Types>
        struct PRINT_TUPLE
        {
            static void print(ostream&os, const tuple<Types...>&t)
            {
                os << std::get<IDX>(t) << (IDX + 1 == MAX ? "" : ",");
                PRINT_TUPLE<IDX + 1, MAX, Types...>::print(os, t);
            }
    
        };
        template<int MAX, typename ...Types>
        struct PRINT_TUPLE<MAX, MAX, Types...>
        {
            static void print(ostream &os, const tuple<Types...>&t)
            {
    
            }
        };
    
        int main()
        {
            cout << make_tuple(7.5, string("hello"), bitset<16>(76), 42) << endl;
            system("pause");
            return 0;
        }
    }
    
    int main(int argc, char* argv[])
    {
        print_tuple::main();
        system("pause");
        return 0;
    }
    

    取最大:

    #include <iostream>
    #include <algorithm>
    using namespace std;
    namespace print_max
    {
        int maximum(int n)
        {
            return n;
        }
    
        template<typename ...Type>
        int maximum(int n, Type ...args)
        {
            return std::max(n, maximum(args...));
        }
        int main()
        {
            cout << maximum(34, 2, 6, 4, 7, 9, 100) << endl;
            system("pause");
            return 0;
        }
    }
    
    int main(int argc, char* argv[])
    {
        print_max::main();
        system("pause");
        return 0;
    }
    

    使用逗号表达式来展开不做介绍。

    类模板:

    可变参数类模板的参数包展开的方式和可变参数函数模板的展开方式不同。

    可变参数类模板的参数包展开需要通过模板特化和继承方式去展开,展开方式比可变参数模板函数要复杂。

    下面我们来看一下使用模板特化的方式去展开参数包。一般有两种展开方式:三段式和两段式。

    三段式定义:

    // 第一部分前向声明,声明这个sum类是一个可变参数模板类,一定要有;
    template <typename ...Args>
    struct Sum;
    
    //第二部分是类的定义:它定义了一个部分展开的可变模参数模板类,告诉编译器如何递归展开参数包。
    template<typename Head, typename ...Tails>
    struct Sum<Head, Tails...>
    {
        enum { Value = Sum<Head>::Value + Sum<Tails...>::Value };
    };
    
    //第三部分是特化的递归终止类:
    template<typename T>
    struct Sum<T>
    {
        enum{ Value = sizeof(T) };
    };
    // 示例
    Sum<int, int, double>::Value;
    

    两段式定义:

    两段式定义,将三段式中的声明去掉了,这样定义:

    template<typename T, typename ...Tail>
    struct Sum
    {
        enum {Value = Sum<T>::Value + Sum<Tail>::Value };
    };
    
    template<typename T>
    struct Sum<T>
    {
        enum { Value = sizeof(T) };
    };
    // 示例
    Sum<int, int, double>::Value;
    

    如果期望展开到0个参数终止,可以添加:

    template<>
    struct sum<> 
    { 
        enum{ value = 0 }; 
    };
    

    接下来的例子我们看一下如何使用继承方式展开参数包。

    #include <iostream>
    #include <string>
    //继承方式展开参数包。
    using namespace std;
    
    namespace tuple_inherited
    {
        template<typename ...Values> class tuple;
    
        template<typename Head, typename ...Tail>
        class tuple<Head, Tail...> :private tuple<Tail...>
        {
            typedef tuple<Tail...> inherited;
        public:
            tuple()
            {
            }
            tuple(Head v, Tail...vtail) :m_head(v), inherited(vtail...)
            {
            }
            Head head()
            {
                return m_head;
            }
    
            inherited& tail()
            {
                return *this;
            }
        protected:
            Head m_head;
        };
    
        template<>
        class tuple<>
        {
    
        };
    
        int main()
        {
            tuple<string, int, float, string>  t("daniel", 4, 5.6, "hello");
            cout << t.head() << endl;
            cout << t.tail().head() << endl;
            system("pause");
            return 0;
        }
    }
    
    int main(int argc, char* argv[])
    {
        tuple_inherited::main();
        system("pause");
        return 0;
    }
    

    使用private继承时,其语义其实是组合,同等地:

    #include <iostream>
    #include <string>
    //组合方式展开参数包。
    namespace tuple_composited
    {
        using std::cout;
        using std::endl;
        using std::string;
        template<typename ...Values>class tuple;
        template<>class tuple<>
        {
        };
        template<typename Head, typename ...Tail>
        class tuple<Head, Tail...>
        {
            typedef tuple<Tail...>composited;
        protected:
            composited m_tail;
            Head m_head;
        public:
            tuple()
            {
            }
            tuple(Head v, Tail...vtail) :m_tail(vtail...), m_head(v)
            {
            }
            Head head()
            {
                return m_head;
            }
            composited &tail()
            {
                return m_tail;
            }
        };
        int main()
        {
            tuple<string, int, float, string>  t("daniel", 4, 5.6, "hello");
            cout << t.head() << endl;
            cout << t.tail().head() << endl;
            system("pause");
            return 0;
        }
    }
    
    int main()
    {
        tuple_composited::main();
        return 0;
    }
    

    参考文献:

    1. 泛化之美--C++11可变模版参数的妙用
    2. C++11 变参模板(variadic templates)
    3. C++11 变参模板
    4. C++模板元编程
    5. C++函数模板
    6. C++模板学习之递归
    7. C++11新标准-1.可变模板参数(variadic templates) - 简书 (jianshu.com)
    8. 印象笔记C++2.0新特性
    9. 侯捷 - C++标准11-14

    相关文章

      网友评论

          本文标题:cpp模板

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