template

多态的一种形式

参数化模块

  • 对程序模块加上类型参数
  • 对不同类型的数据实施相同的操作

模板参数的规则

这一页的重点在于模板参数的灵活性,分为两种情况:

A. 多类型参数 (Multiple Type Parameters)

  • 规则: 模板可以接受多个不同的类型参数,中间用逗号分隔。

  • 语法示例:

    template <class T1, class T2> 
    void f(T1 a, T2 b) { 
        // T1 和 T2 可以是不同类型,比如一个 int 一个 double
        ... 
    }
    

B. 混合普通参数 (Non-Type Parameters / Ordinary Parameters) 这是比较特殊的高级用法,模板里不仅可以传“类型(Type)”,还可以传“数值(Value)”。

  • 规则 1(位置): 普通参数(如 int size)必须列在所有类型参数(如 class T之后

  • 规则 2(调用): 调用时,因为编译器很难根据函数参数推断出这个数值,所以通常需要显式指定模板实参

  • 语法示例:

    // 定义:T 是类型,size 是一个整数常量
    template <class T, int size> 
    void f(T a) { 
        T temp[size]; // 利用传入的 size 在栈上开数组
        ... 
    }
      
    // 调用:必须显式告诉编译器 T是int,size是10
    f<int, 10>(1); 
    

万能引用与完美转发

转发失效

template <typename T>
void transfer(T&& arg) {  // arg 是万能引用
    std::cout << "收到参数..." << std::endl;
    process(arg); // <--- 问题出在这里:arg 有名字,所以它是左值!
}

// 调用端
transfer(std::string("world")); // 传入的是临时对象(右值)

【关键点分析】

  • 场景:我们希望编写一个中间函数 transfer,将参数原封不动地传给 process
  • 期望:传入右值(临时对象),process 应该调用右值版本 void process(string&&) 以获得最高效率。
  • 现实:实际调用了左值版本 void process(const string&)
  • 原因:在 C++ 中,只要变量有名字(如 arg),它就是左值,无论它的类型是不是右值引用。

使用 std::forward 还原参数本色

template <typename T>
void perfectTransfer(T&& arg) {
    // std::forward<T> 根据 T 的类型进行“条件转换”
    process(std::forward<T>(arg)); 
}

【原理解析】

  • 工具std::forward<T>()
  • 作用:它像一个“时光机”,查看参数 arg 在进入函数之前是什么状态。
    • 如果原始参数是左值 $\rightarrow$ 它就以左值形式转发。
    • 如果原始参数是右值 $\rightarrow$ 它就强制转回右值形式转发(相当于 move)。

函数特化

#include <iostream>
#include <cstring>

// 1. 通用模板
template <typename T>
bool areEqual(T a, T b) {
    std::cout << "调用了通用模板" << std::endl;
    return a == b;
}

// 2. 针对 const char* 的显示具体化(全特化)
template <> 
bool areEqual<const char*>(const char* a, const char* b) {
    std::cout << "调用了 const char* 特化版本" << std::endl;
    // 使用 strcmp 比较内容,而不是地址
    return std::strcmp(a, b) == 0;
}

C++ 编译器在决定调用哪个函数时(重载决议),流程是这样的:

  1. 先看所有的普通函数:如果有完全匹配的普通函数(如方案 B),直接选中,结束。
  2. 再看基础模版:如果没有普通函数匹配,才去查基础模版。
  3. 最后看特化:只有确定要用基础模版了,才会去检查“这个模版有没有针对该类型的特化”。

是否实例化模板的某个实例由使用点来决定;如果未使用到一个模板的某个实例,则编译系统不会生成相应实例的代码

模版的完整定义(声明 + 实现)通常必须都写在头文件 (.h) 中

否则会链接错误

错误的代码结构示例:

1. 头文件 (file1.h) - 只有声明

// file1.h
#pragma once

// 类模板声明
template <class T>
class S {
    T a;
public:
    void f(); // 只有函数声明
};

// 函数模板声明
template <class T>
T max(T x, T y);

2. 实现文件 (file1.cpp) - 包含定义

// file1.cpp
#include "file1.h"

// 类模板成员函数的外部定义
template <class T>
void S<T>::f() {
    // ... 具体实现 ...
}

// 函数模板的定义
template <class T>
T max(T x, T y) {
    return x > y ? x : y;
}

// main 函数中只用了 int 版本
void main_in_file1() {
    int a = 1, b = 2;
    max(a, b);   // 编译器生成 max<int> 的代码
    S<int> x;    
    x.f();       // 编译器生成 S<int>::f 的代码
}
// 注意:此时编译器并没有生成 max<double> 或 S<float> 的代码!

3. 调用文件 (file2.cpp) - 链接报错发生地

// file2.cpp
#include "file1.h"

// 或者使用 extern 声明(如 PPT 所示)
// extern double max(double, double); 

void sub() {
    // 【Error 1】链接错误
    // 链接器试图寻找 max<double> 的二进制代码,但在 file1.obj 中找不到。
    max(1.1, 2.2); 

    // 【Error 2】链接错误
    // 链接器试图寻找 S<float>::f 的代码,同样找不到。
    S<float> x;
    x.f(); 
}

if constexpr —— 编译期分支优化

1.问题引入

场景: 我们想写一个“万能转字符串”函数 autoToString

  • 如果传入的已经是 string,直接返回。
  • 如果传入的是数字(int, float等),调用 std::to_string 转换。

错误写法(使用普通 if):

template <typename T> 
std::string autoToString(T val) {
    // std::is_same_v 判断 T 是否为 string
    if (std::is_same_v<T, std::string>) { 
        return val; 
    } else {
        // 问题出在这里!
        return std::to_string(val); 
    }
}

为什么报错?(图中标红的问号)

虽然逻辑上我们知道,当 T 是 string 时会走 if 分支,但是,普通的 if 是运行时判断的。

  • 编译器在编译这段模板时,必须确保两个分支的代码都是合法的
  • Tstd::string 时,编译器会检查 else 分支:return std::to_string(val);
  • 报错原因std::to_string 不接受 std::string 类型的参数。虽然代码永远跑不到这里,但编译器“看到了”它,并报了语法错。
  1. 解决方案 (Slide 2)

引入 if constexpr (C++17)

if constexpr 告诉编译器:“请在编译阶段就计算这个条件。如果条件为真,丢弃 else 分支;如果为假,丢弃 if 分支。”

正确写法:

template <typename T>
std::string autoToString(T val) {
    // 加上 constexpr 关键字
    if constexpr (std::is_same_v<T, std::string>) {
        return val;
    } else {
        return std::to_string(val);
    }
}
  1. 编译器的“剪枝”行为 (核心原理)
  • 当 T = string 时:

    条件为真。编译器直接删除 else 代码块。

    实际生成的代码变成了:

    std::string autoToString(std::string val) {
        return val; 
        // else 里的 std::to_string(val) 根本不存在于最终的二进制代码中,所以不会报错
    }
    
  • 当 T = int 时:

    条件为假。编译器直接删除 if 代码块。

    实际生成的代码变成了:

    std::string autoToString(int val) {
        return std::to_string(val);
    }
    
特性 普通 if if constexpr
生效阶段 运行时 (Runtime) 编译时 (Compile-time)
分支编译 所有分支都必须语法正确,必须能被编译 只有条件满足的分支会被编译,被丢弃的分支甚至可以是语法上对当前类型无效的代码

constexpr来优化代码 原本需要函数特化的部分可以用constexpr来优化

image-20251222144819561

auto 在推导类型时,性格比较“霸道”,它通常会剥离掉原变量的“外衣”(引用和 const),只把最核心的数据类型拿过来做一份拷贝。

  1. “默认情况下是值拷贝”
  • 含义:当你用 auto 去接一个变量时,默认是复制(Copy)一份新的数据,而不是引用原数据。

  • 后果:修改新的变量,不会影响原来的变量。

  • 对应图中代码

    // vec 里面是 {1, 2, 3...}
    for (auto v : vec) {
        v = v * 2; // 无效修改!
    }
    

    这里的 vvec 中元素的副本。你把副本乘以 2 了,但在 vec 里的原版数据纹丝不动。

  1. “丢弃引用和 const 限定符”

这是 auto 最容易让人踩坑的地方。它会把顶层 const(Top-level const)和引用(Reference)全部扔掉。

  • 丢弃引用

    int x = 10;
    int& ref = x; // ref 是 x 的引用
      
    auto a = ref; // 【注意】a 推导出来是 int,而不是 int&
    a = 20;       // x 还是 10,因为 a 只是个拷贝
    
  • 丢弃顶层 const(即变量本身不可变):

    const int b = 10; // b 是不可修改的
      
    auto c = b;       // 【注意】c 推导出来是 int,丢掉了 const
    c = 20;           // c 可以被修改!因为 c 只是拷贝了一份 b 的值,c 自己没说不能改
    
  1. “不会忽略底层 const”

这是唯一的例外!这句话里的括号解释很关键:(指针指向的内容)

  • 含义:如果原来的变量指向了一个“只读区域”(比如字符串字面量),auto 就算再霸道,也不敢把这个“只读”属性丢掉,否则程序就会因为试图修改只读内存而崩溃。

  • 对应图中代码

    // "Hello" 存储在常量区,不可修改
    const char* s = "Hello"; 
      
    auto s_auto = s; 
    // s_auto 推导结果依然是 const char*
    // 如果把 const 丢了变成 char*,你就能修改 "Hello" 了,这在 C++ 里是违法的。
    
原类型 写法 auto 推导结果 说明
int& (引用) auto x = ... int 引用被丢弃,变成值拷贝
const int (常量) auto x = ... int const 被丢弃,变成可读写变量
const char* (底层const) auto x = ... const char* const 被保留,指向的内容依然只读