情景引用:

如果想要实现一个交换函数的重载,我们需要定义好几个函数,一个一个修改他们的类型,费力不说,还容易出错,那有没有什么语法能帮我们解决这个问题呢

void Swap(int& left, int& right)
{
        int temp = left;
        left = right;
        right = temp;
}
void Swap(double& left, double& right)
{
        double temp = left;
        left = right;
        right = temp;
}
void Swap(char& left, char& right)
{
        char temp = left;
        left = right;
        right = temp;
}

有的兄弟(集美)有的!

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

PS:泛型编程泛型编程,意思就是针对广泛的类型进行的编程,不是只针对单一类型编程。

1. 函数模板

1.1 函数模板概念

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

                                        

1.2 函数模板格式

template<typename T1, typename T2,......,typename Tn>

返回值类型 函数名(参数列表){}

假设我们想要实现一个数据结构 vector ,vector 的多个对象,v1 存 int,v2 存 float,等等。在没有模板之前,我们得针对各个类型写很多个 vector 类。而这些函数和类,逻辑都是一样的,只是处理的对象类型不同。

Vector 类模板声明

  • template 关键字用于声明模板

  • <class T1, class T2, ...> 是模板参数列表,可以使用 classtypename 关键字

    template<class T>//注意:class是用来定义模板参数关键字
    //也可以使用typename(切记:不能使用struct代替class)
    void swap(T& x1, T& x2)
    {
        T x = x1;
        x1 = x2;
        x2 = x;
    }
    int main()
    {
            int a = 0, b = 1;
            Swap(a, b);
    
            float c = 1.11, d = 2.22;
            Swap(c, d);
    
            char e = 'e', f = 'f';
            Swap(e, f);
            return 0;
    }

    1.3 模板的原理

    函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。

  •    

    所以我们在调用的时候,调用的并不是同一个函数

    模板的实例化:

    在编译器编译阶段,使用模板函数,编译器需要根据传入的实参类型推演生成对应的函数以供调用。

    例子:当double类型的函数使用该模板时,编译器通过传入的实参,会将T确定为double类型,然后生成一份专门处理double类型的代码,对于其他类型也是如此

    ps:不是在调用时生成,而是在编译前就生成了,模板实例化是在预编译阶段进行的,编译器不会把模板的代码编译放入代码段,而是把模板实例化成对应的函数和类,再把这些函数和类编译成指令进程代码段

    1.4 函数模板的实例化

    1. 隐式实例化:让编译器根据实参推演模板参数的实际类型

    template<class T>
    T Add(const T& left, const T& right)
    {
            return left + right;
    }
    int main()
    {
            int a1 = 10, a2 = 20;
            double d1 = 10.0, d2 = 20.0;
            Add(a1, a2);
            Add(d1, d2);
    
            reuturn 0;
    }

    但是我们不能在 main 函数中调用 Add(a1, d1)。

    该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型,通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错。

    (注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅)

    此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化

        Add(a, (int)d);

    2. 显式实例化:在函数名后的<>中指定模板参数的实际类型

    int main(void)
    {
            int a = 10;
            double b = 20.0;
    
            // 显式实例化
            Add<int>(a, b);
            return 0;
    }

    如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。

    1.5 模板参数的匹配原则

    一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

    // 专门处理int的加法函数
    int Add(int left, int right)
    {
            return left + right;
    }
    // 通用加法函数
    template<class T>
    T Add(T left, T right)
    {
            return left + right;
    }
    void Test()
    {
            Add(1, 2); // 与非模板函数匹配,编译器不需要特化
            Add<int>(1, 2); // 调用编译器特化的Add版本
    }

    2. 类模板

    2.1 类模板的定义格式与实例化

    template<class T1, class T2, ..., class Tn>
    class 类模板名
    {
        // 类内成员定义
    };

  • template 关键字用于声明模板

  • <class T1, class T2, ...> 是模板参数列表,可以使用 classtypename 关键字

  • 类模板不是具体的类,而是编译器生成具体类的"模具"

        2.2类模板的简单实现示例

假设我们想要实现一个数据结构 vector ,vector 的多个对象,v1 存 int,v2 存 float,等等。在没有模板之前,我们得针对各个类型写很多个 vector 类。而这些函数和类,逻辑都是一样的,只是处理的对象类型不同。

Vector 类模板声明

template<class T>
class vector // 动态增长的数组
{
public:
    vector(); // 默认构造函数
    vector(size_t n); // 带参数的构造函数
    ~vector(); // 析构函数
    
    // 成员函数声明
    // 类里面声明,类外面定义
    // 模板传参能用引用就用引用,减少传值
    void push_back(const T& x);
    void push_front(const T& x);
    void pop(const T& x);
    size_t size();
    T operator[](size_t i);
    
private:
    T* _pData; // 存储数据的指针
    size_t _size; // 当前元素数量
    size_t _capacity; // 当前容量
};

构造函数实现

// 默认构造函数
template<class T>
vector<T>::vector()
    : _pData(nullptr)
    , _size(0)
    , _capacity(0)
{}

// 带参数的构造函数
template<class T>
vector<T>::vector(size_t n)
    : _pData(new T[n])
    , _size(0)
    , _capacity(n)
{}

析构函数实现

template<class T>
vector<T>::~vector()
{
    delete[] _pData;
    _pData = nullptr;
    _size = 0;
    _capacity = 0;
}

成员函数的实现

push_back 方法实现
template<class T>
void vector<T>::push_back(const T& x)
{
    // 如果空间不足,需要进行增容
    if (_size == _capacity)
    {
        // 计算新容量
        size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
        
        // 分配新空间
        T* tmp = new T[newcapacity];
        
        // 拷贝旧数据到新空间
        if (_pData != nullptr)
        {
            // 注意:memcpy 拷贝自定义类型时可能导致浅拷贝问题
            // 这里只是简单实现,不做探究
            memcpy(tmp, _pData, sizeof(T) * _size);
            delete[] _pData;
        }
        
        // 更新指针和容量
        _pData = tmp;
        _capacity = newcapacity;
    }
    
    // 添加新元素
    _pData[_size] = x;
    ++_size;
}
其他成员函数实现
// 获取元素数量
template<class T>
size_t vector<T>::size()
{
    return _size;
}

// 下标运算符重载
template<class T>
T vector<T>::operator[](size_t i)
{
    assert(i < _size);
    return _pData[i];
}

2.3 类模板的使用

实例化类模板

int main()
{
    // 实例化 int 类型的 vector
    vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    
    // 遍历 int vector
    for (size_t i = 0; i < v.size(); ++i)
    {
        cout << v[i] << " ";
    }
    cout << endl;
    
    // 实例化 float 类型的 vector
    vector<float> f;
    f.push_back(1.2);
    f.push_back(2.3);
    f.push_back(3.4);
    f.push_back(4.5);
    
    // 遍历 float vector
    for (size_t i = 0; i < f.size(); ++i)
    {
        cout << f[i] << " ";
    }
    cout << endl;
    
    return 0;
}

2.4 重要注意事项

1. 类外定义成员函数的语法

在类模板外部定义成员函数时,需要在每个函数前加上模板声明,并使用类名<T>的形式:

template<class T>
void vector<T>::push_back(const T& x)
{
    // 函数实现
}

2. 模板参数传递建议

  • 尽量使用引用传递,避免不必要的拷贝

  • 对于不会修改的参数,使用 const 引用

3. 嵌套依赖类型与 typename 关键字

什么是嵌套依赖类型?

嵌套依赖类型(Nested Dependent Type)是指在一个模板中,一个类型名称依赖于模板参数,并且是该模板参数内部的嵌套类型。

具体来说,当一个类型满足以下两个条件时,它就是嵌套依赖类型:

  1. 它嵌套在另一个类型内部(如 Container::iterator

  2. 外层类型依赖于模板参数(如 T::inner_typeContainer<T>::iterator

比如 list 部分代码list类中:

template <class T>
void print_list(const list<T>& l)
{
    typename list<T>::const_iterator it = l.begin();    // 这里 list<T> 依赖于模板参数 T
    while (it != l.end())
    {
            cout << *it << " ";
            ++it;
    }
    cout << endl;
}

list<T>::const_iterator 就是一个嵌套依赖类型,因为:

  • const_iterator 嵌套在 list<T> 内部

  • list<T> 依赖于模板参数 T

为什么需要 typename 关键字?

C++ 编译器在解析模板时,需要知道一个依赖名称是类型还是值。由于模板可能被特化,编译器在实例化之前无法确定依赖名称的性质。

使用 typename 关键字可以明确告诉编译器:"这个依赖名称是一个类型"。

其他需要使用 typename 的情况

除了在函数内部声明嵌套依赖类型的变量外,还有以下几种情况需要使用 typename

  1. 作为函数返回类型
    template <class T>
    typename T::value_type get_first(const T& container) {
        return *container.begin();
    }

  2. 作为函数参数类型

    template <class T>
    void process_element(typename T::element_type elem) {
        // 处理元素
    }

  3. 在类模板中声明成员变量类型

    template <class Container>
    class Wrapper {
    public:
        typename Container::value_type first_element; // 使用 typename
    };

  4. 使用模板模板参数中的嵌套类型

    template <template <class> class Container, class T>
    void print_size(const Container<T>& c) {
        typename Container<T>::size_type s = c.size(); // 使用 typename
        cout << s << endl;
    }

  5. 在继承中指定基类类型

    template <class T>
    class Derived : public typename T::BaseType { // 使用 typename
        // ...
    };

    不需要使用 typename 的情况

    以下情况不需要使用 typename

        

  1. 非依赖类型

    template <class T>
    void func() {
        std::string s; // 不需要 typename,因为 std::string 不依赖于 T
        int i;         // 不需要 typename
    }

  2. 基类列表和成员初始化列表

    template <class T>
    class Derived : public T::Nested { // 这里不需要 typename
    public:
        Derived() : T::Nested() {} // 这里也不需要 typename
    };

    代码示例    

    #include <iostream>
    #include <vector>
    #include <list>
    using namespace std;
    
    // 1. 函数返回类型中使用 typename
    template <class Container>
    typename Container::value_type get_first(const Container& c) {
        return *c.begin();
    }
    
    // 2. 函数参数中使用 typename
    template <class Container>
    void print_element(typename Container::value_type elem) {
        cout << elem << " ";
    }
    
    // 3. 类模板中使用 typename
    template <class Container>
    class ContainerInfo {
    public:
        typedef typename Container::value_type value_type; // 使用 typename
        typedef typename Container::iterator iterator;     // 使用 typename
        
        static void print_info() {
            cout << "Container value_type: " << typeid(value_type).name() << endl;
        }
    };
    
    int main() {
        vector<int> vec = {1, 2, 3};
        list<double> lst = {1.1, 2.2, 3.3};
        
        // 测试函数返回类型中的 typename
        cout << "First element of vector: " << get_first(vec) << endl;
        cout << "First element of list: " << get_first(lst) << endl;
        
        // 测试类模板中的 typename
        ContainerInfo<vector<int>>::print_info();
        ContainerInfo<list<double>>::print_info();
        
        return 0;
    }

    总结

  • 嵌套依赖类型是指依赖于模板参数的嵌套类型

  • 使用 typename 关键字告诉编译器某个依赖名称是类型而不是值

  • 在函数返回类型、参数类型、变量声明等地方都可能需要使用 typename

  • 只有在处理依赖类型时才需要使用 typename,非依赖类型不需要

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐