Skip to content

模板与泛型编程

模板是C++最重要的模块之一, 很多人对模板的重视不够, 这一章一定要好好学, 所有课时都是重点。

C++的三大模块, 面向过程, 面向对象, 模板与泛型。面向过程就是C语言, 面向对象就是类, 现在轮到模板与泛型了。

介绍

(1) 模板能够实现一些其他语法难以实现的功能, 但是理解起来会更加困难, 容易导致新手摸不着头脑。 (2) 模板分为类模板和函数模板, 函数模板又分为普通函数模板和成员函数模板。

类模板基础

这段代码非常有代表性

cpp
#pragma once

//这是.hpp文件
template<typename T>
class MyArray {
    using iterator = T*;
    using const_iterator = const T*;
public:
    MyArray(size_t count);
    ~MyArray();
    iterator begin()const;
    const_iterator cbegin()const;
private:
    T* data;
};

template<typename T>
MyArray::MyArray(size_t count) {
    if(count) {
        data = new T[count]();
    }else {
        data = nullptr;
    }
}
template<typename T>
MyArray::~MyArray() {
    if(data) {
        delete[] data;
    }
}

template<typename T>
typename MyArray<T>::iterator MyArray<T>::begin() const {
    return data;
}

template<typename T>
typename MyArray<T>::const_iterator MyArray<T>::cbegin() const {
    return data;
}

实现原理

模板需要编译两次, 在第一次编译时仅仅检查最基本的语法, 比如括号是否匹配。等函数真正被调用时, 才会真正生成需要的类或函数。

所以这直接导致了一个结果, 就是不论是模板类还是模板函数, 声明与实现都必须放在同一个文件中。因为在程序在编译期就必须知道函数的具体实现过程。如果实现和声明分文件编写, 需要在链接时才可以看到函数的具体实现过程, 这当然会报错。

于是人们发明了.hpp文件来存放模板这种声明与实现在同一文件的情况。

initializer_list

介绍

initializer_list其实就是初始化列表, 我们可以用初始化列表初始化各种容器。

cpp
std::initializer_list<int>list{1,2,3,4};
std::vector<int>vec1{1,2,3,4};
std::vector<int>vec2(list);
// 这当然不行了 因为没有对应的构造函数
// MyArray<int> arrayI{1,2,3,4};

(2) 这里的主要任务是在类模板基础的代码中加入initializer_list

cpp
#pragma once
#include <type_traits>

template<typename T>
struct get_type {
    using type = T;
}

template<typename T>
struct get_type<T*> {
    using type = T;
}

template<typename T>
class MyArray {
    using iterator = T*;
    using const_iterator = const T*;
public:
    MyArray(size_t count);
    ~MyArray();
    //左值版本
    MyArray(const std::initializer_list<T>& list);
    //右值版本
    MyArray(std::initializer_list<T>&& list);
    iterator begin()const;
    const_iterator cbegin()const;
private:
    std::vector<T> data;
};

template<typename T>
MyArray::MyArray(size_t count) {
    if(count) {
        data = new T[count]();
    }else {
        data = nullptr;
    }
}
template<typename T>
MyArray::~MyArray() {
    if(data) {
        delete[] data;
    }
}

template<typename T>
typename MyArray<T>::iterator MyArray<T>::begin() const {
    return data;
}

template<typename T>
typename MyArray<T>::const_iterator MyArray<T>::cbegin() const {
    return data;
}

template<typename T>
MyArray<T>::MyArray(const std::initializer_list<T>& list) {
    if(list.size()) {
        unsigned count = 0;
        data = new T[list.size()]();

        if(std::is_pointer<T>::value) {
            for(auto elem : list) {
                data[count++] = new typename get_type<T>::type(*elem);
            }
        }else {   
            for (const auto& elem : list) {
                data[count++] = elem;
            }
        }
    }else {
        data = nullptr;
    }
}
template<typename T>
MyArray<T>::MyArray(std::initializer_list<T>&& list) {
    if(list.size()) {
        unsigned count = 0;
        data = new T[list.size()]();
        for (const auto& elem : list) {
            data[count++] = elem;
        }
    }else {
        data = nullptr;
    }
}

typename

在定义模板时表示这个一个待定的类型。在类外表明自定义类型时使用。

在C++的早期, 为减少关键字数量, 用class来表示模板的参数

但是在 C++ 模板中, 依赖名称是指依赖于模板参数的名称。

在编译模板时, 由于模板参数的具体类型尚未确定, 有时无法判断一个依赖名称是指的是一个类型还是一个非类型成员(例如, 一个静态成员变量或枚举值)。

为明确指出某个名称代表的是一个类型。便引入 typename 关键字。

普通函数模板

普通函数模板的写法与类模板类似。在现代C++中, 函数模板一直普遍使用, 一定要掌握。

cpp
namespace mystd {
    template<typename iter_type, typename func_type>
    void for_each(iter_type first, iter_type last, func_type func) {
        for (auto iter = first; iter != last; ++ iter) {
            func(*iter);
        }
    }
}

int main() {
    std::vector<int> ivec{1, 2, 3, 4, 5};
    // mystd::for_each<std::vector<int>::iterator, void(*)(int& )>(ivec.begin(), ivec.end(), [](int& elem) {
    //     ++elem;  
    // });
    //上面的是完整的 但是可以类型推断 所以可以简写成下面的
    mystd::for_each(ivec.begin(), ivec.end(), [](int& elem) {
        ++elem;  
    });
    for(int elem : ivec) {
        std::cout << elem << std::endl;
    }
    return 0;
}

成员函数模板

成员函数模板使用情况也不少, 需要掌握的

cpp
namespace mystd {
    template<typename T>
    class MyVector {
    public:
        template<typename T2>
        void outPut(const T2& elem);
    };

    template<typename T>
    template<typename T2>
    void MyVector<T>::outPut(const T2& elem) {
        std::cout << elem << std::endl;
    }
}

int main() {
    mystd::MyVector<int> myVec;
    myVec.outPut<int>(20);
    return 0;
}

默认模板参数

默认模板参数: (1) 默认模板参数是一个经常使用的特性, 比如在定义vector对象时, 我们就可以使用默认分配器。

(2) 模板参数就和普通函数的默认参数一样, 一旦一个参数有了默认参数, 它之后的参数都必须有默认参数

(3) 函数模板使用默认模板参数

(4) 类模板使用模板参数

cpp
namespace mystd {
    using func_type = std::function<void(int&)>;
    template<typename iter_type, typename func_type = func_type>
    void for_each(iter_type first, iter_type last, func_type func = [](int& elem) {
        ++elem;
    }) {
        for (auto iter = first; iter != last; ++ iter) {
            func(*iter);
        }
    }
}

int main() {
    std::vector<int> ivec{1, 2, 3, 4, 5};
    mystd::for_each(ivec.begin(), ivec.end());
    for(int elem : ivec) {
        std::cout << elem << std::endl;
    }
    return 0;
}

模板的重载

函数模板是可以重载的(类模板不能被重载), 通过重载可以应对更加复杂的情况。

比如在处理 char*string 对象时, 虽然都可以代表字符串, 但 char* 在复制时直接拷贝内存效率明显更高。

string 就不得不依次调用构造函数了。所以在一些比较最求效率的程序中对不同的类型进行不同的处理还是非常有意义的。

cpp
template<typename T>
void test(const T& parm) {
    std::cout << "void test(const T& parm)" << std::endl;
}

template<typename T>
void test(T* parm) {
    std::cout << "void test(T* parm)" << std::endl;
}

void test(double parm) {
    std::cout<< "void test(double parm)" << std::endl;
}

其实函数模板的重载和普通函数的重载没有什么区别。

模板的特化

函数模板可以重载以应对更加精细的情况。类模板不能重载, 但可以特化来实现类似的功能。

全特化

就是指模板的实参列表与与相应的模板参数列表一一对应。

cpp
template<>
class Test<int, int> {
public:
    Test() {std::cout << "int, int complete special" << std::endl;}
}

偏特化

偏特化就是介于普通模板和全特化之间, 只存在部分类型明确化, 而非将模板唯一化。

cpp
template<typename T1, typename T2>
class Test{
public:
    Test() {std::cout << "common template" << std::endl;}
};

template<typename T1, typename T2>
class Test<T1*, T2*>{
public:
    Test() {std::cout << "point semi-template" << std::endl;}
};

template<typename T2>
class Test<int, T2> {
public:
    Test() {std::cout << "int semi-special" << std::endl;}
};