现代C++学习——模板和泛型编程
本文最后更新于 537 天前,其中的信息可能已经有所发展或是发生改变。

模板引入

  • 模板与泛型,是与 面向过程 面向对象 并列的 C++ 重要部分,远不止是面向对象的延伸
  • 主要分为类模板和函数模板,函数模板又包含普通函数和成员函数

类模板和模板概述

以下为一个简单的数组类模板:

template<typename T>
class MyArray
{
    using iterator = T*;
    using const_iterator = const T*;    // 类型重命名,更易读
public:
    MyArray(size_t count);  // size_t宏定义为unsigned,大小与机器位数有关(这里是uint64_t)
    ~MyArray();

    iterator begin()const;  // 函数体之前包括const都属于声明
    const_iterator cbegin()const;
private:
    T* data;    // 暂时没摸索出智能指针分配数组的方法
};
  1. 模板的基本标志是在声明前使用 template<typename T>,待定类型

  2. 模板的实现原理:两次编译

    • 初次编译只检查基本语法,因为声明不包含逻辑
    • 第二次编译,对模板的实现代入类型,检查语法并编译成实际的类

    关于模板声明和定义写在一起的分析:

    1. 对于普通函数/类,其声明存放在头文件作为接口,其实现放在源文件用于运行时调用

    2. 所以头文件和源文件的区别,大致是:

      • 源文件是固定的实现,单独编译后直接链接,在运行期直接调用

      • 头文件必须在编译期跟随源文件编译

    3. 模板函数/类的实现,虽然语法完整但缺少具体类型支持,是半成品,不能跳过编译直接链接,必须在编译期代入类型生成具体实现(也就是模板的第二次编译)

    4. 所以模板的实现和声明必须都写进头文件

    • 于是,C++ 提供了 .hpp 专用于模板这种声明和定义都要写进头文件的情况

    • 你肯定好奇模板是不是破坏了头文件的整洁性?

      是的。头文件也是 C++ 的远古遗产,C++20 开始引入模块概念正是着手解决头文件使用时的笨重问题

template <typename T>
MyArray<T>::MyArray(size_t count) {
    if(count) {
        data = new T[count]();
    } else {
        data = nullptr;
    }
}
// 每个函数前都要加 template
template <typename T>
MyArray<T>::~MyArray() {
    if(data) {
        delete[] data;
    }
}
// 使用类下的自定义类型,typename 关键字作用是注明 :: 后面的不是类成员而是自定义类型
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;
}
// 有关函数模板的细节,后面再详解

以上为上面类成员函数的类外实现(类内实现略)

作为类的成员,定义的访问就需要使用类的全名,即带上 <T>

initializer_list 和 typename

initializer_list

C++11 标准库提供的类型,可以用于数组形式的数据。语法特点是作为参数可省略圆括号,此外都是标准库实现

image-20231020000616717

上图展示了其格式,以及可作为右值形式

std::initializer_list<int> iList {1, 2, 3};
std::vector<int> ivec {1, 2, 3};  // 实现了构造函数 vector(std::initializer_list<T>, 后面的参数略)

对自定义类实现 initializer_list 的支持:

// 萃取
#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>
MyArray<T>::MyArray(std::initializer_list<T>& list){
    if(list.size()) {
        unsigned count = 0;
        data = new T[list.size()]();
        if(std::is_pointer_v<T>) {    // type_traits提供的函数(C++17),返回 bool
            for(auto elem : list) {
                data[count++] = new typename get_type<T>::type(*elem);
            }
        }else {
            for(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(auto& elem : list) {
            data[count++] = elem;
        }
    }else {
        data = nullptr;
    }
}

这句最初理解错了,记录一下:data[count++] = new typename get_type<T>::type(*elem);

typename get_type<T>::type 访问了特化模板下取得的类型,*elem 取得了列表中指针指向的值

所以说是将动态分配的指针数组,其中每个指针指向一块新的数据,实现深拷贝

  • 错解原因:

    1. <T> 迷惑,傻傻分不清类型到底是指针还是指向类型
    2. 尚不熟悉模板下的类型,分不清类型和变量
  • 如果只是线性存储的话,可以将 T* data 换成 vector<T>,但也可以手写析构,不难

    vector 自带析构,且提供了便利的初始化等函数,如果是存放指针,搭配智能指针可以实现内存自由

    // 假定存放了动态分配的传统指针(只是个例子,建议能用模板就换模板)
    template 
    MyArray::~MyArray() {
      if(data) {
          if(std::is_pointer_v) {
              for(auto i = 0; i < length; i++) {
                  delete data[i];
              }
          }
          delete[] data;
      }
    }
  • 拷贝构造中,这里区别处理 initializer_list,普通对象传引用,指针传值(都传值也行,看深拷贝需求)

    移动拷贝中,不存在以上问题

  • 指针的判断:

    1. 模板特化:此处只是用于获取指向类型,后面会详解
    2. 萃取技术:<type_traits> 提供的一系列函数
  • 复习一下 C++ 传参(学习初期容易认为值和指针一无是处,所以有必要补充):

    1. 传指针(包括智能):非必要不用,要么是库的规定,要么是需要指针作为变量的场合,要么是整花活
    2. 传引用:不需要指针,只需要指向对象(对象包括指针)的场合,安全性较高,比值高效,所以最常用
    3. 传值:特点是拷贝,与外界变量隔离安全性最高。也是非必要不用,因为拷贝明显有低效的缺点
typename

两种用途:

  1. 声明模板类 / 函数时,声明为待定类型 T template<typename T>
  2. 使用模板类下的类型 类::custom_type 时,标明其为一个类型,而非静态成员
  • 早期使用 class 关键字平替,后来出现了上面第二种情况,就诞生了新的关键字 typename

  • 注:C++20 标准已经支持在类自定类型前省略 typename,旧标准缺少关键字时 IDE 会提示需要切换新标准

  • 所以对于这个关键字什么时候该加的讨论纯浪费时间,可以到此为止了

函数 / 成员函数模板

基本语法与类没有太大区别

使用时会自动判断类型,也可以像类一样显式确定类型(但是纯多此一举),如下:

// 自定义一个普通函数for_each执行一个简单回调
namespace mgc
{
    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};
    mgc::for_each(ivec.begin(), ivec.end(), [](int& elem) {
            ++elem;
        });
    mgc::for_each<std::vector<int>::iterator, void(*)(int&)>(ivec.begin(), ivec.end(), [](int& elem) {
            ++elem;
        });     // 显式确定类型
    for (const auto value : ivec) std::cout << value << std::endl;  // 3 4 5
}

以下为在类模板中另外定义函数模板:

template <typename T>
class MyVec{
public:
    template <typename T2>
    void func1(const T2& elem){
        std::cout << elem << std::endl;
    }   // 类内定义

    template <typename T2>
    void func2(const T2& elem);
};
// 两层template
template <typename T>
template <typename T2>
void MyVec<T>::func2(const T2& elem){
    std::cout << elem;
}   // 类外定义

template 的使用,凡是下一个声明用到了待定类型,都要在前面加 template

template 数量与模板数量相关,如上方的类和成员函数,是两个独立模板,所以要写两行

默认模板参数

函数模板,在声明待定类型时可以赋默认类型。与参数一样,默认类型一旦声明,也必须遵循从右到左规则

函数模板默认参数
  1. 如果函数声明的参数有默认值,则模板对应类型必须有默认值,如果不指定,则该参数无效

    // function目前可以认为是C++11提供的函数指针升级版
    template >
    void test(int& i, func_type func = [](int& e){e++;}){
    func(i);
       cout << i;
    }
    // 参数可以不赋值,但参数赋值之后待定类型必须赋值
    
    test(5);
  2. 与默认参数一样,默认类型是隐式的,在使用时可以被覆盖,如果显式使用了其他类型的参数,则类型会隐式覆盖

    前提是该模板函数支持那个类型

    模板的默认类型是给默认参数服务的,并不是限制类型

    比如上方函数默认参数是 int& 的 lambda 表达式,所以必须先确定 func_type 来通过编译

    假如实际调用时,传入的不是 int& 的 lambda 表达式,那么也不用管 func_type 的默认值了,只要函数体能跑通就行,如下

    // 函数定义
    template ::iterator, typename func_type = function>
    test(iter_type first, iter_type last, func_type func = [](int& elem){elem++;});
    // 以下是无视默认值调用,是等效的
    std::vector svec {"mgc", "xcr", "mcg"};
    
    test(svec.begin(), svec.end(), [](string str){cout << str;});    // 隐式覆盖类型
    
    test::iterator, void(*)(string)>(svec.begin(), svec.end(), [](string str){cout << str;}); // 显示覆盖类型(依然只是起到注解功能罢了)
  3. 总结:

    • 函数为待定类型指定默认值,需要声明模板默认类型。也就是模板函数声明默认值的硬性附加条件
    • 其他情况都是起到注释的作用,提供一种支持的类型,可以无视只要替换的类型能兼容逻辑即可
类模板默认参数
  1. 在默认类型上,语法与函数模板完全一致

  2. 示例——STL 中的容器普遍使用了默认类型 allocator<T>,用于当前容器 container<T> 的内存管理

    所以创建容器一般给元素类型就行了,也可以写个适配各种操作的模板类放进去

    template >
  3. 类模板的默认参数同样可以被无视,只要替换的类型能兼容默认类型的操作即可

模板重载和特化

在这一节,你可以体会到指针所指向的类型具有前所未有的存在感

模板重载

函数模板专用

与普通函数重载一样,也是对不同类型使用不同逻辑

区别:

  • 普通函数只能按某一个类型重载,而模板函数可以按某一类类型重载(比如重载一个给指针用的版本)

  • 普通函数重载与类型一一对应,函数模板重载存在匹配优先级:

    非模板重载 > 模板重载 > 模板非特化,如果 const & 之类不匹配,就降一级

template <typename T>
void test(const T& t){
    std::cout << "template test(const T& t)" << std::endl;
}   // 模板非特化

template <typename T>
void test(T* t){
    std::cout << "template test(T* t)" << std::endl;
}   // 模板重载

void test(int t){
    std::cout << "test(int t)" << std::endl;
}   // 非模板重载

void test(int* t){
    std::cout << "test(int* t)" << std::endl;
}   // 非模板重载

test(100);  // test(int t)
test(ivec); // template test(const T& t)
int i = 10;
test(&i);   // test(int* t)
double d = 10;
test(&d);   // template test(T* t)
模板特化

模板提供的,支持同名模板使用不同类型列表实现变体的方式,与重载的区别是声明带有 <特化类型>

  • 对于函数,重载完全够用了,而且再用特化也容易混淆。类不能重载,特化可以说是给类用的

  • 全特化:确定全部类型,只匹配类型一一对应的对象

  • 偏特化:在原始模板基础上进行一些改动,缩小匹配范围(如确定部分类型,或改为匹配指针)

    • 特化如果确定了类型,会减少待定类型,全特化没有待定类型,开头为 template<>
template <typename T1, typename T2>
class Test{
public:
    Test(){ std::cout << "原始模板" << std::endl; }
};

template <typename T1, typename T2>
class Test<T1*, T2*>{
public:
    Test(){ std::cout << "指针偏特化" << std::endl; }
};

template <typename T1>
class Test<T1&, double>{
public:
    Test(){ std::cout << "混合偏特化" << std::endl; }
};

template <typename T2>
class Test<int, T2>{
public:
    Test(){ std::cout << "偏特化" << std::endl; }
};

template <>
class Test<int, double>{
public:
    Test(){ std::cout << "全特化" << std::endl; }
};

因为对象的定义必须要 <类型>,所以不用像函数那样只传值而混淆,优先度如下:

全特化 > 确定类型的偏特化 > 只特化了指针或引用的偏特化 > 原始模板

  • 禁止混淆匹配,比如:
  template 
  class Test{};
  template 
  class Test{};

  Test test;    // 匹配失败,报错
  • 利用特化匹配优先度,实现的根据指针获取指向类型(原始模板只是提供特化基础):

    template 
    class get_type{
      using type = T;
    }
    template 
    class get_type{
      using type = T;
    }
    
    typename get_type::type i = 0;  // int
文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇