• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2026-08-06 19:32 Aet 隐藏边栏 |   抢沙发  2 
文章评分 1 次,平均分 5.0

函数模板与实例化

模板是什么

  1. 普通函数只能处理声明好的类型
    1. 如果还要处理 doublestd::string,不使用模板就需要重复编写

  1. 模板的作用是
    1. 描述一组具有相同结构、但类型不同的函数
    2. 这段代码本身不是一个普通函数,而是一个函数模板,也可以理解为编译器生成函数的“配方”

模板参数和函数参数

  1. 这里有两种完全不同的参数

名称 示例 作用
模板参数 T 在编译期代表一种类型
函数参数 ab 在运行期保存具体数值
  1. typename T 表示
    1. T 是一个类型参数
  2. 在声明类型模板参数时,classtypename 含义相同
    1. T 不要求一定是类,也可以是 int 等基础类型,因此我通常推荐 typename

示例

编译器遇到调用时做了什么

  1. 第一步:发现函数模板

  1. 第二步:推导 T

  1. 第三步:替换模板参数
    1. max_value<int> 称为函数模板的一个特化

  1. 第四步:实例化
    1. 当程序确实需要这个函数时,编译器根据模板生成相应定义
    2. 这个过程称为实例化
  2. 总结,需要区分:
    1. 模板:生成代码的规则
    2. 模板实参:例如 int
    3. 特化:例如 max_value<int>
    4. 实例化:根据模板产生对应特化的过程

显式指定模板实参

  1. 也可以直接告诉编译器 T 是什么
    1. 因为 T 已经指定为 double7 会转换为 double

  1. 概念上调用的是

为什么混合类型会失败

  1. 下面通常无法通过编译
    1. 同一个 T 得到两个冲突结果,推导失败

  1. 一个非常重要的规则是:
    1. 模板参数推导期间,编译器通常不会先进行任意隐式类型转换来帮助推导

模板并不真的支持“任意类型”

  1. 书中经常说 T 可以代表任意类型。这句话容易误解
  2. 更准确地说:
    1. T 可以替换成任意类型,但替换之后形成的代码必须合法
  3. 当前模板使用了:
    1. 因此 T 必须支持相应的 < 操作,并且结果能够用于条件判断

  1. 当前函数按值接收和返回对象
    1. 所以还涉及 T 的构造、复制或移动能力

  1. C++20 中,我们可以通过 Concepts 把这些要求直接写进接口

为什么模板通常写在头文件中

  1. 普通函数可以这样组织

  1. 但函数模板通常必须让使用它的编译单元看到完整定义,因为编译器需要根据具体的 T 实例化代码
    1. 所以常见结构是:

  1. 大型项目中也经常使用

练习

多个模板参数、返回类型推导与函数模板重载

单一模板参数的限制

  1. 下面可以推导

  1. 下面会失败

  1. 因为推导发生冲突

多个模板参数

  1. 调用

  1. 推导结果

  1. 返回表达式为
    1. 由于条件运算符需要为第二、第三个操作数寻找兼容类型,int 会转换为 double,所以最终返回 double

为什么不能直接返回 T1

  1. 它虽然能编译,但返回类型依赖参数顺序

显式设置返回类型

  1. 一种方法是增加返回类型模板参数

  1. 模板参数可以只显式指定前面一部分,剩余参数继续推导

C++14 auto返回类型推导

  1. C++14 开始,可以让编译器根据 return 表达式推导返回类型
    1. 这里的 auto 和变量声明中的 auto 类似,通常会移除顶层引用和顶层 const,返回一个值

  1. 需要注意:
    1. 如果函数中有多个 return,每个返回表达式必须推导出相同类型

C++11 的尾置返回类型

  1. C++11 还不能直接根据函数体推导普通函数的 auto 返回类型,因此当时常写成:
    1. 之所以使用尾置返回类型,是因为只有声明完 ab 后,才能在返回类型中使用它们

  1. 这里存在一个严重陷阱
    1. 返回的引用会悬空,使用它会产生未定义行为

  1. 严格的 C++11 写法为

std::common_type

  1. C++11 标准库提供了 std::common_type,用于寻找多个类型的公共类型

函数模板重载

  1. 函数模板可以和普通函数同名

  1. 调用
    1. 两个候选都能精确匹配
    2. 当匹配质量相同时,普通函数优先

转换质量优先于“普通函数优先”

  1. 调用

  1. 所以规则不是“永远优先普通函数”,而是
    1. 先比较参数匹配质量;只有其他条件相同时,才优先普通函数

强制选择模板

  1. 可以使用空模板实参列表
    1. 这表示只考虑函数模板,并由实参推导 T

  1. 也可以显式指定

两个函数模板之间的选择

  1. 调用
    1. 两个模板都匹配,但第二个要求两个参数必须具有相同类型,限制更严格,因此它更特化,编译器选择第二个

  1. 第二个模板推导冲突,只有第一个可用

返回类型不能单独构成重载

  1. 调用
    1. 编译器不能根据接收返回值的位置选择函数

  1. 因此
    1. 返回类型不参与普通的函数重载选择
  2. 如果需要不同返回类型,应
    1. 显式添加返回类型模板参数
    2. 使用不同函数名
    3. 增加能够参与重载的参数
    4. 使用 Tag DispatchConcepts 或策略类型

一个容易忽略的危险调用

  1. 假设有

  1. 调用

  1. 这再次说明
    1. 模板推导失败不代表整个调用失败,编译器还会继续检查其他重载

类模板

概述

  1. 函数模板描述“一组函数”,类模板描述“一组类”

  1. 它不是一个具体类型,而是生成类型的模板

  1. 核心规则是
    1. 类模板本身不是具体类型;填入模板实参后形成的 Stack<int> 才是类型

最小类模板

实现 Stack<T>

在类外定义成员函数

  1. 如果成员函数定义在类模板外面,需要重新写出模板声明

  1. 为什么必须写 Stack<T>::

类内部可以省略 <T>

  1. 在类模板内部

示例

不同模板实参产生不同类型

类模板的成员不会一次性全部实例化

  1. 假设

  1. 定义一个不可输出的类型

  1. 但不是所有错误都会延迟

成员函数与成员函数模板不是一回事

  1. 这里的 push() 依赖类模板参数 T,但它本身没有自己的模板参数
    1. 它是类模板的成员函数,不是独立的成员函数模板

  1. 真正的成员函数模板还会再出现一层 template
    1. T 属于类模板
    2. U 属于成员函数模板

push(T&&) 不是转发引用

  1. 这里的 T&& 是普通右值引用,不是转发引用

  1. 转发引用

  1. 判断方法:
    1. 如果 T 在进入函数调用之前就已经由外层类固定了,那么 T&& 通常不是转发引用

为什么模板实现通常放在头文件

  1. 只在头文件声明

  1. 然后在 .cpp 定义

  1. 但其他编译单元使用时通常看不到模板完整定义,无法实例化 Stack<int>::push(),最终容易出现链接错误

  1. 常见组织方式有两种

    1. 全部写入 .hpp

    2. 声明和实现分成 .hpp.tpp

      .tpp 最终还是被头文件包含,所以使用点能看到完整定义

类模板特化、默认参数、别名与 CTAD

主模板

  1. 如果不存在更匹配的特化版本,就使用主模板

全特化

  1. 假设希望为 bool 提供完全不同的实现

  1. 语法结构
    1. 这里写 template<>,因为已经没有需要继续推导的模板参数
    2. 所有模板实参都确定了,因此称为全特化或显式特化

全特化是一个独立的类

  1. 全特化不会继承主模板的成员

全特化的成员函数定义

  1. 全特化
    1. 定义其成员函数时,不再写 template<>
    2. 因为 Stack<std::string> 已经是一个确定的类

  1. 头文件中的 ODR 问题
    1. 全特化的类定义可以放在头文件
    2. 类内定义的成员隐式为 inline

  1. 但如果在头文件中这样写:
    1. 已经是普通非模板函数定义
    2. 多个 .cpp 包含后可能违反 ODR

特化必须在首次实例化之前可见

  1. 全特化必须在第一次会导致隐式实例化的使用之前声明,并且各个编译单元必须看到一致的特化声明

  1. 通常把主模板和特化都放在相关头文件中

偏特化

  1. 全特化处理一个确定类型

  1. 偏特化处理一整类具有某种结构的类型

  1. 注意这里仍然存在未知类型 T
    1. 所以前面必须保留template<typename T>

示例

为什么类模板可以偏特化

  1. 简化版 is_pointer

  1. 顶层 const 指针问题

多模板参数的偏特化

  1. 定义主模板

  1. 如果两个类型相同

  1. 如果第二个类型为 int

偏特化可能产生歧义

  1. 两者没有谁明显比另一个更特殊,所以产生歧义

  1. 可以为交集提供更具体的完全特化

函数模板不能偏特化

  1. 类模板可以

  1. 函数模板不能这样做

  1. 函数模板通常使用重载

默认类模板参数

  1. 可以把底层容器也参数化

  1. 也可以覆盖默认参数

  1. 如果两个都有默认值

类型别名

别名模板

  1. using 可以被模板化

别名模板不能被特化

  1. 下面不允许

  1. 如果需要特化,应增加一层类模板

C++17类模板参数推导CTAD

  1. C++17 之前通常必须写

  1. C++17 开始,如果构造函数参数能够推导模板参数,可以省略 <...>

  1. CTAD 只是在构造对象时帮助确定类模板实参,并没有把 Stack 变成普通类型

CTAD与字符串字面量

  1. 假设构造函数按值接收

  1. 重要的一点
    1. 推导指引只决定构造哪个类型,不负责真正执行参数转换或对象构造

直接初始化与拷贝初始化

示例

其他

std::decay

  1. std::decayC++11 引入,定义在
    1. #include <type_traits>

  1. 规则一:移除引用和顶层 cv
    1. 普通类型会先移除引用,然后移除顶层的 constvolatile

  1. 规则二:数组退化成元素指针

  1. 规则三:函数类型退化成函数指针

怎么理解decay

  1. std::decay<T> 大致模拟了:
    1. 当一个对象按值传递或使用普通 auto 保存时,类型会发生什么变化

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享