《C++高级编程》读书笔记(十二:利用模板编写泛型代码)

news2024/10/7 6:48:31

1、参考引用

  • C++高级编程(第4版,C++17标准)马克·葛瑞格尔

2、建议先看《21天学通C++》 这本书入门,笔记链接如下

  • 21天学通C++读书笔记(文章链接汇总)

1. 模板概述

  • 模板将参数化的概念推进了一步,不仅允许参数化值,还允许参数化类型。C++ 中的类型不仅包含原始类型,例如 int 和 double,还包含用户定义的类,例如 SpreadsheetCell 和 CherryTree。使用模板,不仅可编写不依赖特定值的代码,还能编写不依赖那些值类型的代码

2. 类模板

  • 类模板定义了一个类,其中,将一些变量的类型、方法的返回类型和/或方法的参数类型指定为参数。类模板主要用于容器,或用于保存对象的数据结构

2.1 编写类模板

  • 假设想要一个通用的棋盘类,可将其用作象棋棋盘、跳棋棋盘、井字游戏棋盘或其他任何二维的棋盘。为让这个棋盘通用,这个棋盘应该能保存象棋棋子、跳棋棋子、井字游戏棋子或其他任何游戏类型的棋子
2.1.1 Grid 类定义
  • 第一行表示,下面的类定义是基于一种类型的模板。就像在函数中通过参数名表示调用者要传入的参数一样,在模板中使用模板参数名称 (例如 T) 表示调用者要指定的类型
  • 指定模板类型参数时,可用关键字 class 替代 typename,但 class 会产生一些误解,因为这个词暗示这种类型必须是一个类,而实际这种类型可以是 class、struct、union、基本类型如 int 或 double 等
    template <typename T>
    class Grid {
        // ...
    };
    
2.1.2 Grid 类的方法定义
  • template <typename T> 访问说明符必须在 Grid 模板的每一个方法定义的前面
  • 模板要求将方法的实现也放头文件中,因为编译器在创建模板的实例前,需知道完整的定义,包括方法的定义
    template <typename T>
    Grid<T>::Grid(size_t width, size_t height) : mWidth(width), mHeight(height) { // 构造函数
        // ...
    }
    
2.1.3 使用 Grid 模板
  • 创建网格对象时,不能单独使用 Grid 作为类型,必须指定这个网格保存的元素类型。为某种类型创建一个模板类对象的过程称为模板的实例化。下面举一个示例

    Grid<int> myIntGrid;
    Grid<double> myDoubleGrid(11, 11);
    
    myIntGrid.at(0, 0) = 10;
    // at() 方法返回 std:optional 引用。optional 可包含值,也可不包含值
    // 如果 optional 包含值,value_or() 方法返回这个值;否则返回给 value_or() 提供的实参
    int x = myIntGrid.at(0, 0).value_or(0);
    
    Grid<int> grid2(myIntGrid); // 复制构造函数
    Grid<int> anotherIntGrid;
    anotherIntGrid = grid2; // 赋值运算符
    
  • 如果要声明一个接收 Grid 对象的函数或方法,必须在 Grid 类型中指定保存在网格中的元素类型

    void processIntGrid(Grid<int>& grid) {
        // ...
    }
    
  • 为避免每次都编写完整的 Grid 类型名称,例如 Grid<int>,可通过类型别名指定一个更简单的名称

    using IntGrid = Grid<int>;
    
  • Grid 模板能保存的数据类型不只是 int。例如,可实例化一个保存 SpreadsheetCell 的网格

    Grid<SpreadsheetCell> mySpreadsheet;
    SpreadsheetCell myCell(1.234);
    mySpreadsheet.at(3, 4) = myCell;
    
  • Grid 模板还可保存指针类型

    Grid<const char*> myStringGrid;
    myStringGrid.at(2, 2) = "hello";
    
  • Grid 模板指定的类型甚至可以是另一个模板类型

    Grid<vector<int>> gridOfVectors;
    vector<int> myVector{1, 2, 3, 4};
    gridOfVectors.at(5, 6) = myVector;
    
  • Grid 模板还可在堆上动态分配 Grid 模板实例

    auto myGridOnHeap = make_unique<Grid<int>>(2, 2);
    myGridOnHeap->at(0, 0) = 10;
    int x = myGridOnHeap->at(0, 0).value_or(0);
    

2.2 编译器处理模板的原理

  • 编译器遇到模板方法定义时会进行语法检查但不编译模板。编译器无法编译模板定义,因为它不知道要使用什么类型
  • 编译器遇到一个实例化的模板时,例如 Grid<int> myIntGrid,就会将模板类定义中的每一个 T 替换为int,从而生成 Grid 模板的 int 版本代码。当编译器遇到这个模板的另一个实例时,就会生成另一个版本的 Grid 类

2.3 将模板代码分布在多个文件中

  • 通常情况下,将类定义放在一个头文件中,将方法定义放在一个源代码文件中。创建或使用类对象的代码会通过 #include 来包含对应的头文件,通过链接器访问这些方法代码
  • 模板不按这种方式工作。由于编译器需要通过这些 “模板” 为实例化类型生成实际的方法代码,因此在任何使用了模板的源代码文件中,编译器都应该能同时访问模板类定义和方法定义。有好几种机制可以满足这种包含需求
2.3.1 将模板定义放在头文件中
  • 方法定义可与类定义直接放在同一个头文件中。当使用了这个模板的源文件通过 #include 包含这个文件时,编译器就能访问需要的所有代码。此外,还可将模板方法定义放在另一个头文件中,然后在类定义的头文件中通过 #include 包含这个头文件
    // Grid.h
    template <typename T>
    class Grid {
        // ...
    };
    // 一定要保证方法定义的 #include 在类定义之后,否则代码无法编译
    #include "GridDefinition.h"
    
2.3.2 将模板定义放在源文件中
  • 可将方法定义放在一个源代码文件中。然而,仍然需要让使用模板的代码能访问到定义,因此可在模板类定义头文件中通过 #include 包含类方法实现的源文件
    // Grid.h
    template <typename T>
    class Grid {
        // ...
    };
    
    #include "Grid.cpp"
    

2.4 模板参数

2.4.1 非类型的模板参数
  • 非类型的模板参数只能是整数类型(char、int、long 等)、枚举类型、指针、引用和 std::nullptrt。从 C++17 开始,也可指定 auto、auto& 和 auto* 等作为非类型模板参数的类型,此时,编译器会自动推导类型
    template <typename T, size_t WIDTH, size_t HEIGHT>
    class Grid {
        // ...
    };
    // 实例化模板
    Grid<int, 10, 10> myGrid;
    Grid<int, 10, 10> anotherGrid;
    myGrid.at(2, 3) = 42;
    anotherGrid = myGrid;
    cout << anotherGrid.at(2, 3).value_or(0);
    
2.4.2 类型参数的默认值
  • 如果继续采用将高度和宽度作为模板参数的方式,就可能需要为高度和宽度 (它们是非类型模板参数) 提供默认值
    template <typename T = int, size_t WIDTH = 10, size_t HEIGHT = 10>
    class Grid {
        // ...
    };
    
    // 不需要在方法定义的模板规范中指定 T、WIDTH 和 HEIGHT 的默认值
    template <typename T, size_t WIDTH, size_t HEIGHT>
    const std::optional<T>& Grid<T, WIDTH, HEIGHT>::at(size_t x, size_t y) const {
        // ...
    }
    
    // 实例化 Grid 时,可不指定模板参数,只指定元素类型,或者指定元素类型和宽度,或者指定元素类型、宽度和高度
    Grid<> myGrid;
    Grid<int> myGrid2;
    Grid<int, 5> myGrid3;
    Grid<int, 5, 5> myGrid4;
    
2.4.3 构造函数的模板参数推导
  • C++17 添加了一些功能,支持通过传递给类模板构造函数的实参自动推导模板参数。在 C++17 之前,必须显式地为类模板指定所有模板参数
  • 例如,标准库有一个类模板 std:pair(在 <utility> 中定义)。pair 存储两种不同类型的两个值,必须将其指定为模板参数
    std::pair<int, double> pair1(1, 2.3);
    
  • 为避免编写模板参数的必要性,可使用一个辅助的函数模板 std:make_pair()。函数模板始终支持基于传递给函数模板的实参自动推导模板参数。因此,make_pair() 能根据传递给它的值自动推导模板类型参数
    auto pair2 = std::make_pair(1, 2.3);
    
  • C++17 中,不再需要这样的辅助函数模板,编译器可以根据传递给构造函数的实参自动推导模板类型参数
    std::pair pair3(1, 2.3);
    

    推导的前提是类模板的所有模板参数要么有默认值,要么用作构造函数中的参数
    std::unique_ptr 和 shared_ptr 会禁用类型推导,需要继续使用 make_unique() 和 make_shared() 创建

2.5 方法模板

  • C++ 允许模板化类中的单个方法,这些方法可以在类模板中,也可以在非模板化的类中

    不能用方法模板编写虚方法和析构函数

  • 不能将类型为 Grid<int> 的对象赋给类型为 Grid<double> 的对象,也不能从 Grid<int> 构造 Grid<double>

  • Grid 复制构造函数和 operator= 都接收 const <Grid> 的引用作为参数

    Grid(const Grid<T>& src);
    Grid<T>& operator=(const Grid<T>& rhs);
    
  • 当实例化 Grid<double> 并试图调用复制构造函数和 operator= 时,编译器通过这些原型生成方法

    Grid(const Grid<double>& src);
    Grid<double>& operator=(const Grid<double>& rhs);
    
  • 在生成的 Grid<double> 类中,构造函数或 operator= 都不接收 Grid<int> 作为参数,但可通过双重模板解决:在 Grid 类中添加模板化的复制构造函数和赋值运算符,可生成将一种网格类型转换为另一种网格类型的方法

    template <typename T>
    class Grid {
    public:
        // ...
        template <typename E>
        Grid(const Grid<E>& src);
    
        template <typename E>
        Grid<T>& operator=(const Grid<E>& rhs);
    };
    
    • 下面是新的复制构造函数的定义,必须将声明类模板的那一行 (带有 T 参数) 放在成员模板的那一行声明 (带有 E 参数) 的前面
    template <typename T>
    template <typename E>
    Grid<T>::Grid(const Grid<E>& src) : Grid(src.getWidth(), src.getHeight()) {
        // ...
    }
    

2.6 类模板的特例化

  • 模板的另一个实现称为模板特例化,通过这项特性,当模板类型被特定类型替换时,可为模板编写特殊实现
  • 编写一个模板类特例化时,必须指明这是一个模板,以及正在为哪种特定的类型编写这个模板。下面是为 const char* 特例化
    // 下述语法告诉编译器,这个类是 Grid 类的 const char* 特例化版本
    template <>
    class Grid<const char*> {
        // ...
    };
    
    Grid<int> myIntGrid;
    Grid<const char*> stringGrid(2, 2);
    

    注意,在这个特例化中不要指定任何类型变量,例如 T,而是直接处理 const char*

  • 特例化的主要好处就是可对用户隐藏:当用户创建 int 或 SpreadsheetCell 类型 Grid 时,编译器从原始 Grid 模板生成代码;当用户创建 const char* 类型的 Grid 时,编译器会使用 const char* 的特例化版本,这些全部在后台自动完成
  • 特例化一个模板时,并没有继承任何代码:特例化和派生化不同,必须重新编写类的整个实现,不要求提供具有相同名称或行为的方法
    • 例如,Grid 的 const char* 特例仅实现 at() 方法,返回 std::optional<std::string> 而非 std::optional<const char*>

2.7 从类模板派生

  • 可从类模板派生。如果一个派生类从模板本身继承,那么这个派生类也必须是模板。此外,还可从类模板派生某个特定实例,这种情况下,这个派生类不需要是模板
  • 假设通用的 Grid 类没有提供足够的棋盘功能。确切地讲,要给棋盘添加 move() 方法,允许棋盘上的棋子从一个位置移动到另个位置。下面是这个 GameBoard 模板的类定义
    • 这个GameBoard 模板派生自 Grid 模板,因此继承了 Grid 模板的所有功能
    • 继承的语法和普通继承一样,区别在于基类是 Grid<T>,而不是 Grid
    #include "Grid.h"
    
    template <typename T>
    class GameBoard : public Grid<T> {
    public:
        explicit GameBoard(size_t width = Grid<T>::kDefaultWidth,
                           size_t height = Grid<T>::kDefaultHeight);
        void move(size_t xSrc, size_t ySrc, size_t xDest, size_t yDest);
    };
    

2.8 继承还是特例化

  • 通过继承来扩展实现和使用多态,通过特例化自定义特定类型的实现
    在这里插入图片描述

3. 函数模板

  • 还可为独立函数编写模板。例如,可编写一个通用函数,该函数在数组中查找一个值并返回这个值的索引
    // size_t 是一个无符号整数类型
    // 通过这样的转换,可以将负值转换为等效的正值,以便在使用无符号整数时表示特殊的未找到或无效状态
    static const size_t NOT_FOUND = static_cast<size_t>(-1);
    
    template <typename T>
    // 这个 Find() 函数可用于任何类型的数组
    size_t Find(const T& value, const T* arr, size_t size) {
        for (size_t i = 0; i < size; ++i) {
            if (arr[i] == value) {
                return i;
            }
        }
        return NOT_FOUND;
    }
    
  • 与类模板方法定义一样,函数模板定义(不仅是原型)必须能用于使用它们的所有源文件。因此,如果多个源文件使用函数模板,或使用前面讨论的显式实例化,就应把其定义放在头文件中
  • 函数模板的模板参数可以有默认值,与类模板一样

3.1 函数模板的特例化

template<>
size_t Find<const char*>(const char* const& value, const char* const* arr, size_t size) {
    for (size_t i = 0; i < size; ++i) {
        if (strcmp(arr[i], value) == 0) {
            return i;
        }
    }
    return NOT_FOUND;
}
const char* word = "two";
const char* words[] = {"one", "two", "three", "four"};
const size_t sizeWords = std::size(words);
size_t res;

res = Find<const char*>(word, words, sizeWords);
res = Find(word, words, sizeWords);

3.2 对模板参数推导的更多介绍

  • 编译器根据传递给函数模板的实参来推导模板参数的类型,而对于无法推导的模板参数,则需要显式指定。例如,如下 add() 函数模板需要三个模板参数:返回值的类型以及两个操作数的类型
    template <typename RetType, typename T1, typename T2>
    RetType add(const T1& t1, const T2& t2) {
        return t1 + t2;
    }
    
  • 调用这个函数模板时,可指定如下所有三个参数
    auto result = add<long long, int, int>(1, 2);
    
  • 但由于模板参数 T1 和 T2 是函数的参数,编译器可以推导这两个参数,因此调用 add() 时可仅指定返回值的类型
    auto result = add<long long>(1, 2);
    
  • 也可提供返回类型模板参数的默认值,这样调用 add() 时可不指定任何类型
    template <typename RetType = long long, typename T1, typename T2>
    RetType add(const T1& t1, const T2& t2) {
        return t1 + t2;
    }
        
    auto result = add(1, 2);
    

3.3 函数模板的返回类型

  • 让编译器推导返回值的类型岂不更好?确实是好,但返回类型取决于模板类型参数,从C++14 开始,可要求编译器自动推导函数的返回类型
    template <typename T1, typename T2>
    auto add(const T1& t1, const T2& t2) {
        return t1 + t2;
    }
    
  • 但是,使用 auto 来推导表达式类型时去掉了引用和 const 限定符,C++14 以后可使用 decltype(auto) 编写 add() 函数,以避免去掉任何 const 和引用限定符
    template <typename T1, typename T2>
    decltype(auto) add(const T1& t1, const T2& t2) {
        return t1 + t2;
    }
    

4. 可变模板

  • 除了类模板、类方法模板和函数模板外,C++14 还添加了编写可变模板的功能
    template <typename T>
    constexpr T pi = T(3.14159265);
    
  • 上述是 pi 值的可变模板。为了在某种类型中获得 pi 值,可使用如下语法
    float piFloat = pi<float>;
    long double piLongDouble = pi<long double>;
    
  • 这样总会得到在所请求的类型中可表示的 pi 近似值。与其他类型的模板一样,可变模板也可以特殊化

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/694755.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用Python搭建监控平台详解

概要 监控和运维&#xff0c;是互联网工业链上非常重要的一环。监控的目的就是防患于未然。通过监控&#xff0c;我们能够及时了解到企业网络的运行状态。一旦出现安全隐患&#xff0c;你就可以及时预警&#xff0c;或者是以其他方式通知运维人员&#xff0c;让运维监控人员有时…

oracle 自定义类型(type)的用法

emp表数据如下所示 定义object类型 create or replace type typeof_userinfo_row as object(user_id varchar2(50),user_name varchar2(50) )创建函数并将此类型作为返回值类型 create or replace function FUN_TEST return typeof_userinfo_row isFunctionResult typeof_use…

互联网业务全球化互通组网

随着互联网业务的快速发展&#xff0c;越来越多的企业开始全球化扩张业务&#xff0c;并需要在全球范围内建立互联网组网以实现业务数据的高效传输。在这个过程中&#xff0c;如何建立高效、稳定的全球互联网组网方案&#xff0c;是每个企业都需要考虑的问题。 一种可行的方案…

springboot 上传文件

在Spring Boot中&#xff0c;可以使用RequestParam注解来接收文件。 在你的控制器方法中&#xff0c;使用RequestParam注解来声明一个MultipartFile类型的参数来接收上传的文件 MultipartFile是spring类型&#xff0c;代表HTML中form data方式上传的文件&#xff0c;包含二进制…

【C语言操作符优先级】

C语言操作符优先级 C语言操作符1、操作符的优先级序表2、操作符的属性2.1、操作符优先级运算例程12.2、操作符优先级运算例程2 C语言操作符 前言&#xff1a; &#xff08;1&#xff09;在我们熟悉的数学加减乘除运算中&#xff0c;都知道先乘除再加减&#xff0c;有括号的先算…

Error: Cannot find module ‘webpack‘ 问题解决办法

这句话的意思是&#xff1a;没有找到webpack模块。 就算之前你装了webpack&#xff0c;那肯定是非全局安装 所以要全局安装 npm install --save-dev webpack 问题解决

数据库管理-第八十六期 19c OCM之路-第一堂(01)(20230628)

数据库管理 2023-06-28 第八十六期 19c OCM之路-第一堂&#xff08;01&#xff09;1 环境补充说明2 第一堂-01考点1&#xff1a;Create a pluggable database 创建PDB考点2&#xff1a;Create tablespace 创建表空间考点3&#xff1a;Managing undo 管理undo 总结 第八十六期 1…

【无名管道】无名管道(Pipe)与进程通信:快速上手

目录 0. 管道概述&#xff1a; 1. 管道特点 2. 管道创建&#xff1a;pipe函数 3. 管道的读写特点 4. 通过fcntl函数设置文件的阻塞特性 5. 查看管道缓冲区命令 总结&#xff1a; 0. 管道概述&#xff1a; 管道也叫无名管道&#xff0c;它是是 UNIX 系统 IPC&#xff08;…

019、数据库管理之备份恢复管理(BR)

备份的重要性 数据库恢复审计和分析典型DBA任务 备份的类型 热备&#xff0c;允许应用程序完全访问数据。冷备&#xff0c;不允许应用程序访问年数据温备&#xff0c;允许应用程序读取&#xff0c;但不能修改 热备份 热备份是在读取和修改数据时进行的&#xff0c;几乎不会…

怎么高效批量调整视频的尺寸大小

大家平时在剪辑视频的过程中&#xff0c;是否会遇到视频尺寸大小不合适的情况&#xff0c;当我们遇到这种情况时&#xff0c;如何才能快速批量地解决呢&#xff1f;有没有什么方法可以快速批量修改视频尺寸大小呢&#xff1f;跟着小编一起来看看我平时批量修改视频尺寸的方法。…

联邦学习的架构思想

目录 联邦学习介绍&#xff08;非常详细&#xff09; 联邦学习的由来 联邦学习的发展历程 1) 机器学习 2) 分布式机器学习 3) 隐私保护技术 4) 联邦学习 联邦学习的规范与标准 联邦学习的架构思想 联邦学习的社区与生态 联邦学习介绍&#xff08;非常详细&#xff09…

Lenovo联想笔记本电脑 小新 Air-14 2020 Intel平台IIL版(81YJ)原装Win10系统恢复原厂OEM预装专用系统

Lenovo联想笔记本电脑&#xff0c;小新 Air-14 2020 Intel平台IIL版(81YJ)出厂Windows10系统原装系统镜像&#xff0c;恢复原厂状态 系统自带所有驱动、出厂主题壁纸LOGO、Office办公软件、联想电脑管家等预装程序 所需要工具&#xff1a;16G或以上的U盘 文件格式&#xff1…

基于微信小程序的上课签到系统(数据库+报告+文档+演示视频)

系统技术语言介绍&#xff1a; 本系统采用了SSM (Spring Spring MVC Mybatis&#xff09;架构&#xff0c; MySQL作为基础数据库&#xff0c;微信开发工具作为前端基础&#xff0c;前端采用了 wxml的设计语言&#xff0c;Idea作为后台的开发工具。 功能列表&#xff1a; 1.…

从数字图像到音视频学习:我的学习之旅

数字图像是一门广泛应用于计算机视觉、图像处理和计算机图形学等领域的学科&#xff0c;而音视频学习则涵盖了音频和视频的处理、分析和应用。 如果你最开始接触数字图像&#xff0c;可能会学习一些基本概念&#xff0c;例如像素、分辨率、色彩空间和图像处理算法等。这可能涉…

java项目之药源购物网站ssm源码+文档

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的药源购物网站。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xff1a;风歌&#xff…

隔离变压器和浪涌保护器SPD有哪些不同作用

浪涌保护装置&#xff08;SPD&#xff09;和隔离变压器的作用经常混淆。为了保护敏感的电子设备和设备免受瞬态事件的影响&#xff0c;必须考虑各种因素。 无论瞬态来自外部还是内部&#xff0c;信号噪声、共模瞬变、差模瞬变和接地电位上升都可能导致保护不力的设备出现问题。…

vue中使用mock.js

安装mock npm install mockjs --save-dev或者在ui中选择依赖 查看安装是否成功 进入package.json文件 配置mock 在src目录下新建mock文件夹&#xff0c;在mock文件夹下建立index.js 配置模拟请求数据 index.js import Mock from mockjsconst loginData Mock.mock(http://l…

【 云原生 kubernetes 】- 单点登录Authelia + OpenLdap

文章目录 简介AutheliaOpenLdap属性介绍 Helm部署openldap配置调整数据持久化部署使用命令行Web Authelia 页面访问反向代理进行集成流程 ⚡️: OpenLDAP是轻量级目录访问协议&#xff08;LDAP&#xff09;的开源实现&#xff0c;它提供了一种存储和访问关于用户、组、计算机和…

使用Python将图片转pdf

使用Python将图片转pdf 1. 效果图2. 源码参考 今天需要把图片转pdf&#xff0c;发现迅捷pdf转换开始默认带水印了&#xff0c;收费版那算了&#x1f602;&#x1f602;&#xff0c;那就用Python吧&#xff0c;可单转&#xff0c;可批量转。 参考: https://blog.csdn.net/qq_53…

我是如何在linux下 安装 mongoDB的

背景 接到上级任务&#xff0c;需要在测试环境搭建一套mongodb&#xff1b;对于从来没接触过mongodb的小白&#xff0c;但拥有多年编程经验的我来说&#xff0c;不虚好吧。因为我有自己做事的思考。 首先向领导确认两点&#xff1a; 搭建mongo db是单机版的&#xff0c;还是主…