C++模板编程:从入门到精通

2. 模板

  • 模板:
    • 一种源代码复用机制
    • 参数化模块
      • 对程序模块加上类型参数
      • 对不同类型的数据实施相同的操作
    • 模板是多态的一种形式
  • 解决同一操作作用于不同类型数据的问题

不方便的方式:

  • 类属函数的宏实现

    cpp 复制代码
    #define max(a,b) ((a) > (b) ? (a) : (b))
    • 缺陷:
      • 无类型检查(宏定义是在预处理阶段展开的, 编译器无法进行类型检查)
      • 只能实现简单的功能
  • 函数重载

    • 缺陷:
      • 需要为每种类型都定义一个函数
      • 代码重复, 难以维护, 而且容易定义不全
  • 函数指针

    • void sort(coid *, unsigned int, unsigned int, int (*cmp)(const void*, const void*));
    • 缺陷:
      • 需要进行强制类型转换, 不安全
      • 代码复杂, 可读性差

综上, template 引入的目标 : 完全, 清晰的模板

2.1 函数模板

cpp 复制代码
template <typename T>
void sort(T A[], unsigned int n){
    //排序算法
    for (unsigned int i=0; i<n-1; i++)
        for (unsigned int j=i+1; j<n; j++)
            if (A[i] > A[j]) {
                T temp = A[i];
                A[i] = A[j];
                A[j] = temp;
            }
}
int main(){
    int a[] = {3,5,1,4,2};
    sort<int>(a,5);//显示指定类型参数
    double b[] = {3.1,5.2,1.3,4.4,2.5};
    sort(b,5);//隐式指定类型参数
}

对于内置类型可以使用, 对于自定义操作类型也可以:

  • 必须重载操作符 >
  • 必须有拷贝构造
  • 必须有赋值操作符

模板:

  • 函数模板定义了一类重载的函数

  • 编译系统自动实例化函数模板

    • 可以有多个类型参数, 用逗号分隔

      cpp 复制代码
      template <typename T1, typename T2>
      void func(T1 a, T2 b){ ... }
  • 函数模板的参数:

    • 可带普通参数:必须列在类型参数之后, 调用时需显式模板实参指定(编译期常量 )
      template <typename T, int size> void func(T a){ T temp[size]; ... }
      f<int, 10>(a);
万能引用和完美转发

下面代码存在问题: 传入的参数 arg 无论是左值还是右值, 在传递给 process 函数时都会被视为左值, 导致无法调用处理右值的重载版本.

cpp 复制代码
void process(const std::string& s); // 处理左值
void process(std::string&& s);      // 处理右值

template <typename T>
void transfer(T&& arg) {
    std::cout << "收到参数" << std::endl;
    process(arg); // 这里的 arg 是右值引用, 实际上是左值
}

为了解决这个问题, 我们可以使用 std::forward 来实现完美转发, 保持参数的值类别(左值或右值)不变.

  • 如果 T 推导出来是左值, 那么就转发为左值
  • 如果 T 推导出来是右值, 那么就转发为右值
  • 这里的 T&& 被称为万能引用 (Universal Reference)或转发引用(Forwarding Reference)
cpp 复制代码
template <typename T>
void perfectTransfer(T&& arg) {
    std::cout << "收到参数" << std::endl;
    process(std::forward<T>(arg)); // 完美转发
}

int main() {
    std::string str = "Hello";
    perfectTransfer(str);               // 传入左值
    perfectTransfer(std::string("Hi")); // 传入右值
    // 消解引用的方式: 引用折叠
}
显式具体化(全特化)
  • 首先是通用模板形式, 后面我们给出显式具体化
cpp 复制代码
template <typename T>
bool isEqual(T a, T b){
    return a == b;
}

int main() {
    // Case1: 整数
    int x = 5, y = 5;
    std::cout << "整数比较: " << isEqual(x, y) << std::endl; // true
    // Case2: 字符串
    const char* str1 = "hello";
    const char* str2 = "hello";
    std::cout << "字符串比较: " << isEqual(str1, str2) << std::endl; // true//false 不确定, 因为是指针, 取决于编译器是否优化两个 str1 和 str2 指向同一内存地址, 我们这里需要显式具体化, 否则不保证"字符串相等"的语义
}

// 显式具体化 for const char*
template <>
bool isEqual<const char*>(const char* a, const char* b){
    return std::strcmp(a, b) == 0;
}
  • STL中的实际应用: std::vector<bool> 进行性能优化

2.2 类模板

  • 定义了若干个类
  • 显式模板实参指定 (C++17引入了类模板参数推导, 可以隐式指定)
  • 可以带有多个参数, 可以带有普通参数
  • 类模板中的静态成员属于实例化之后的类, 而不是模板本身
cpp 复制代码
template <typename T, int size>
class Stack{
    T buffer[size];
    int top;
public:
    void push(T x);
    T pop();
};

template <typename T, int size>
void Stack<T, size>::push(T x){...}

template <typename T, int size>
T Stack<T, size>::pop(){...}

int main(){
    Stack<int, 100> intStack;
    Stack<double, 50> doubleStack;
}
C++ 中模板的完整定义通常放在头文件中
  • 因为模板是在编译时实例化的, 编译器需要在编译阶段看到模板的完整定义, 才能生成相应的代码
  • 如果模板定义放在源文件中, 编译器在编译其他源文件时无法访问模板定义, 导致链接错误
cpp 复制代码
// file1.h
template <typename T>
class S{
    T a;
public:
    void f();
};
// file1.cpp
#include "file1.h"
template <typename T>
void S<T>::f(){...;}
template <class T>
T max(T a, T b){
    return (a>b)? a:b;
}
void main(){
    int a, b;
    max(a,b);
    S<int> x;
    x.f();
}
// file2.cpp
extern double max(double, double); // 声明
void sub() {
    double x = 1.1, y = 2.2;
    double m = max(x, y); // 链接错误, 因为找不到 max 的定义
    S<float> s; // 链接错误, 因为找不到 S<float> 的定义
    s.f();
}
  • 问题在于: 如果在模块A中要使用模块B中定义的某模板的实例,而在模块B中未使用这个实例,则模块A无法使用这个实例
if constexpr 解决编译问题:

问题代码:

cpp 复制代码
template <typename T>
std::string autotoString(T value) {
    if(std::is_same_v<T, std::string>) {
        return value; 
    } else {
        return std::to_string(value); 
    }
}

int main() {
    std::string str = "Hello, World!";
    std::cout << autotoString(str) << std::endl; // 错误! 因为 std::to_string(std::string) 不存在
}

解决方案, 使用 if constexpr:

cpp 复制代码
template <typename T>
std::string autotoString(T value) {
    if constexpr (std::is_same_v<T, std::string>) {
        return value; 
    } else {
        return std::to_string(value); 
    }
}
  • 如果 T 是 std::string, 编译器直接删除 else 分支
  • 如果 T 不是 std::string, 编译器直接删除 if 分支
相关推荐
shoubepatien3 小时前
JAVA -- 05
java·开发语言
寰天柚子3 小时前
Java并发编程中的线程安全问题与解决方案全解析
java·开发语言·python
沐知全栈开发3 小时前
Bootstrap 下拉菜单:设计与实现指南
开发语言
memgLIFE3 小时前
Springboot 分层结构
java·spring boot·spring
Queenie_Charlie3 小时前
HASH表
数据结构·c++·哈希算法
shoubepatien3 小时前
JAVA -- 08
java·后端·intellij-idea
kong79069283 小时前
Java新特性-(二)Java基础语法
java·新特性·java 基础语法
yangminlei3 小时前
springboot pom.xml配置文件详细解析
java·spring boot·后端
Evand J3 小时前
【MATLAB例程】多锚点RSSI定位和基站选择方法,基于GDOP、基站距离等因素。以Wi-Fi定位为例,附下载链接
开发语言·matlab·定位·gdop·rssi