欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

第 17 课 - C++ [模板进阶]

最编程 2024-10-13 07:24:36
...

????前言

模板作为搭建STL的关键工具以及泛型编程思想的核心体现,对提高程序灵活性和推动高效迭代开发具有重要意义。除了基本的类型替换功能外,模板还具备如非类型模板参数、全特化、偏特化等高级操作。同时,模板声明与定义不能分离的问题也值得深入探讨。

????️正文

1、非类型模板参数

  • 1.1 使用方法
    • 以往模板参数多是用于匹配不同类型,如intdoubleDate等。但实际上模板参数还可匹配常量(非类型),用于确定如数组、位图等结构的大小。定义非类型模板参数时,不再使用classtypename,而是直接使用具体类型,如size_t。需注意,非类型模板参数必须为常量,且在编译阶段确定值。
    • 例如,利用非类型模板参数定义一个大小可*调整的整型数组类:
    • cpp
template <size_t N> 
class arr 
{ 
public: 
    int& operator[](size_t pos) 
    { 
        assert(pos >= 0 && pos < N); 
        return _arr[pos]; 
    } 

    size_t size() const 
    { 
        return N; 
    } 

private: 
    int _arr[N]; 
};

main函数中可以这样使用:

  • cpp
int main()
{
    arr<10> a1; 
    arr<20> a2; 
    arr<100> a3; 
    cout <<"a1 size():"<<a1.size() <<endl;
    cout <<"a2 size():"<<a2.size() <<endl;
    cout <<"a3 size():"<<a3.size() <<endl;
    return 0;
}

输出结果为:

a1 size():10
a2 size():20
a3 size():100

进一步地,如果再加入一个模板参数(类型),就可得到一个泛型且大小可自定义的数组:

  • cpp
template <class T, size_t N> 
class arr 
{ 
public: 
    T& operator[](size_t pos) 
    { 
        assert(pos >= 0 && pos < N); 
        return _arr[pos]; 
    } 

    size_t size() const 
    { 
        return N; 
    } 

private: 
    T _arr[N]; 
};
  • 使用示例:
  • cpp
int main()
{
    arr<int, 10> a1;
    arr<double, 20> a2;
    arr<char,100> a3;
    cout << typeid(a1).name() <<endl;
    cout<<typeid(a2).name()<<endl;
    cout<<typeid(a3).name()<<endl;
    return 0;
}
  • 非类型模板参数还支持缺省,例如:template <class T, size_t N = 10>

  • 1.2 类型要求

    • 非类型模板参数要求类型为整型家族,其他类型不符合标准。
    • 例如:
    • cpp
//整型家族(部分) 
template <class T, int N> 
class arr1 { /*……*/ }; 

template <class T, long N> 
class arr2 { /*……*/ }; 

template <class T, char N> 
class arr3 { /*……*/ };
  • 而使用其他家族类型作为非类型模板参数会引发报错,
  • 比如:
  • cpp
//浮点型,非标准 
template <class T, double N> 
class arr4 { /*……*/ };

会出现错误提示:浮点模板参数是非标准的(在 C++20 标准中或许有相关引入,但部分编译器可能仍不支持)。

  • 总结来说,非类型模板参数只能将整型家族类型作为参数,且必须为常量,在编译阶段确定结果。整型家族包括charshortboolintlonglong long等。

  • 1.3 实际例子:array

    • C++官网:array - C++ Reference (cplusplus.com)
    • C++11标准中,新容器array使用了非类型模板参数,是一个真正意义上的泛型数组,用于对标传统数组。
    • cpp
#include <iostream> 
#include <cassert> 
#include <array> 

using namespace std; 

int main() 
{ 
    int arrOld[10] = { 0 }; 
    array<int, 10> arrNew; 

    //与传统数组一样,新数组未初始化 
    //新数组越界读、写检查更严格 

    arrOld[15]; 
    arrNew[15]; 

    arrOld[12] = 0; 
    arrNew[12] = 10; 
    return 0; 
}
  • array是泛型编程思想的产物,支持STL容器的一些功能,如迭代器和运算符重载等,主要改进是严格检查越界行为。不过在实际开发中,由于其对标传统数组且连初始化都没有,功能和实用性上不如vector,并且使用栈区空间存在栈溢出问题,所以使用相对较少。其严格检查越界行为是通过在进行下标相关操作前,对传入的下标进行合法性检验实现的,如assert(pos >= 0 && pos < N)

2、模板特化

  • 2.1 概念

通常模板可实现与类型无关的代码,但在某些场景中,泛型无法满足精准需求,会引发错误。例如使用日期类对象指针构建优先级队列时,若不编写对应的仿函数,比较结果会是未定义的。

例如:

如果传递的不是指针是正常的:

  如果传的是地址的话,地址每一次都不一样,不能满足需求;

模板特化就是在原模板基础上进行特殊化处理,创造出符合特定需求的 “特殊” 模板。

  • 2.2 函数模板特化

    • 函数模板也支持特化。例如下面的比较函数,如果不进行特化,对于字符串比较会出现错误结果:
    • cpp
template <class T> 
bool isEqual(T x, T y) 
{ 
    return x == y; 
} 

int main() 
{ 
    int x = 10; 
    int y = 20; 
    cout << "x == y: " << isEqual(x, y) << endl; 

    char str1[] = "Haha"; 
    char str2[] = "Haha"; 
    cout << "str1 == str2: " << isEqual(str1, str2) << endl; 
    return 0; 
}
  • 原因是字符串比较时,泛型比较的是地址而非内容。解决方法是利用模板特化为字符串比较构建特殊模板:
  • cpp
//函数模板特殊,专为char*服务 
template <> 
bool isEqual<char*>(char* x, char* y) 
{ 
    return strcmp(x, y) == 0; 
}
  • 2.3 类模板特化
    • 类模板特化可解决大部分特殊问题,分为全特化和偏特化。
    • 2.3.1 全特化
      • 全特化是将所有模板参数特化为具体类型,全特化后的模板在调用时会优先被选择。例如:
      • cpp
//原模板 
template <class T1, class T2> 
class Test 
{ 
public: 
    Test(const T1& t1, const T2& t2) 
        : _t1(t1), _t2(t2) 
    { 
        cout << "template<class T1, class T2>" << endl; 
    } 

private: 
    T1 _t1; 
    T2 _t2; 
} ; 

//全特化后的模板 
template <> 
class Test<int, char> 
{ 
public: 
    Test(const int& t1, const char& t2) 
        : _t1(t1), _t2(t2) 
    { 
        cout << "template<>" << endl; 
    } 

private: 
    int _t1; 
    char _t2; 
} ; 

int main() 
{ 
    Test<int, int> T1(1, 2); 
    Test<int, char> T2(20, 'c'); 
    return 0; 
}

在进行全特化前需要存在基本的泛型模板,全特化模板中的模板参数可以不写,但要在类名之后指明具体参数类型,否则无法实例化对象。

  • 2.3.2 偏特化
    • 偏特化是将泛型范围进一步限制,可以限制为某种类型的指针或具体类型。
    • 例如:
    • cpp
//原模板---两个模板参数 
template <class T1, class T2> 
class Test 
{ 
public: 
    Test() 
    { 
        cout << "class Test" << endl; 
    } 
} ; 

//偏特化之一:限制为某种类型 
template <class T> 
class Test<T, int> 
{ 
public: 
    Test() 
    { 
        cout << "class Test<T, int>" << endl; 
    } 
} ; 

//偏特化之二:限制为不同的具体类型 
template <class T> 
class Test<T*, T*> 
{ 
public: 
    Test() 
    { 
    cout << "class Test<T*, T*>" << endl; 
    } 
} ; 

int main() 
{ 
    Test<double, double> t1; 
    Test<char, int> t2; 
    Test<Date*, Date*> t3; 
    return 0; 
}

偏特化在泛型思想和特殊情况之间做了折中处理,在进行偏特化前需要存在基本的泛型模板,且要注意与全特化区分。

3、模板的分离编译问题

  • 3.1 失败原因
    • 当模板声明与定义分离后,在链接时无法在符号表中找到目标地址进行跳转,从而导致链接错误。
    • 例如,当模板声明与定义写在同一个文件中时,如Test.hmain.cpp
      Test.h文件内容:

cpp

#pragma once 

//声明 
template <class T> 
T add(const T x, const T y); 

//定义 
template <class T> 
T add(const T x, const T y) 
{ 
    return x + y; 
}
  • main.cpp文件内容:
  • cpp
#include <iostream> 
#include "Test.h" 

using namespace std; 

int main() 
{ 
    add(1, 2); 
    return 0; 
}

可以正常运行。但当声明与定义分离时,编译器无法确定函数原型,无法生成函数,也就无法获得函数地址,在符号表中进行函数链接时必然失败。

  • 3.2 解决方法
    • 解决方法有两种:
      • 在函数定义时进行模板特化,编译时生成地址以进行链接,但如果类型较多,不推荐这种方法,因为需要特化很多份。
      • 例如:
      • cpp
//定义 
//解决方法一:模板特化(不推荐,如果类型多的话,需要特化很多份) 
template <> 
int add(const int x, const int y) 
{ 
    return x + y; 
}
  • 模板的声明和定义不要分离,直接写在同一个文件中。例如:
  • cpp
//定义 
//解决方法二:声明和定义写在同一个文件中 
template <class T> 
T add(const T x, const T y) 
{ 
    return x + y; 
}

这也是为什么涉及模板的类,其中的函数声明和定义通常写在同一个文件(.h)中的原因,如STL库中的代码。为了区分,也可将头文件后缀改为.hpp,如Boost库中的一些命名方式。


4.模板中必须使用typename关键字而非class的场景

一、模板中的依赖名称

当在模板中使用一个依赖于模板参数的名称,且这个名称可能是一个类型也可能是一个值时,编译器无法确定它到底是不是一个类型。

  • 例如:
  • cpp
template<typename T>
void func(T t) {
    T::iterator it; // 这里编译器不知道 T::iterator 是不是一个类型
//T::iterator 可能是T类中的静态变量,也有可能是一个类(类部类)
}

在这种情况下,如果T::iterator确实是一个类型,就必须使用typename来明确告知编译器:

  • cpp
template<typename T>
void func(T t) {
    typename T::iterator it; // 使用 typename 明确表示这是一个类型
}

二、避免二义性

使用typename可以避免与class关键字可能引起的二义性。当使用class时,编译器可能会将其理解为一个类名的定义,而不是表示一个类型。

  • 例如:cpp
template<typename T>
class MyClass {
public:
    void doSomething() {
        typename T::SomeType st; // 使用 typename 明确类型
        // 如果这里使用 class,可能会被误解为类的定义而不是类型
    }
};

总之,在 C++ 模板中,为了明确表示一个依赖于模板参数的名称是一个类型,应该使用typename而不是class,以避免编译器的不确定性和二义性。


5、模板小结

  • 模板是STL的基础支撑,具有诸多优点。
    • 它复用了代码,节省资源,推动了更快的迭代开发,是C++标准模板库(STL)产生的基础。
    • 增强了代码的灵活性。
  • 同时也存在一些缺点。
    • 模板会导致代码膨胀,增加编译时间。
    • 出现模板编译错误时,错误信息复杂,不易定位。

????总结

本文详细介绍了 C++ 模板进阶的相关内容,包括非类型模板参数、模板特化以及模板的分离编译问题。非类型模板参数可用于确定结构大小,有特定的使用方法和类型要求;模板特化分为函数模板特化和类模板特化,可解决泛型无法满足的特殊需求;模板声明与定义分离会导致链接错误,可通过特化或不分离来解决。总之,模板虽有优缺点,但合理使用可使代码更灵活、更优雅。后续还会继续探索C++进阶内容,如继承、多态、高阶二叉树等知识点。