C++ 学习(八)(模板,可变参数模板,模板专业化(完整模板专业化,部分模板专业化),类型 Traits,SFINAE(替换失败不是错误),)

C++ 模板

C++ 中的模板是一项强大的功能,允许您编写通用代码,这意味着您可以编写可以处理不同数据类型的单个函数或类。这意味着您无需为要使用的每种数据类型编写单独的函数或类。

模板函数

要创建模板函数,请使用 关键字,后跟类型参数或用尖括号 () 括起来的占位符。然后,您可以像往常一样定义函数,使用类型参数指定泛型类型。template``< >

下面是一个简单的模板函数示例,它接受两个参数并返回两个参数中较大的一个:

template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

要使用此功能,您可以显式指定 type 参数:

int result = max<int>(10, 20);

或者,您可以让编译器为您推断类型:

int result = max(10, 20);

我们可以用万能模具来比喻C++中的模板功能:


模板是什么?

想象你有一个万能模具,这个模具本身是空心的,但当你往里面倒入不同材料(数据类型)时,它会自动生成对应材料的成品(具体函数或类)。


举个生活例子

假设你要做三种形状完全相同的杯子,但材料不同:

  • 陶瓷杯 🍶

  • 玻璃杯 🥛

  • 不锈钢杯 🥤

传统做法 :为每种材料单独制作一个模具(为每个类型写单独的函数)
模板做法:只需制作一个万能模具,倒入不同材料自动生成对应杯子


对应代码示例

// 万能模具(模板函数)
template<typename T>  // T就是材料类型占位符
T max(T a, T b) {     // 用T声明参数和返回值类型
    return (a > b) ? a : b;
}

// 使用时:
max<int>(10, 20);      // 倒入陶瓷 → 生成陶瓷杯子(比较int)
max<double>(3.14, 2.7); // 倒入玻璃 → 生成玻璃杯子(比较double)
max<char>('A', 'C');    // 倒入不锈钢 → 生成不锈钢杯子(比较char)
复制代码

关键特性

  1. 自动适配 :编译器会根据你使用的类型自动生成对应版本的函数

    max(10, 20);    // 自动识别为int版本(C++11起可省略<类型>)
    max(3.14, 2.7); // 自动识别为double版本
    
    复制代码
  2. 类型安全:比宏定义更安全(宏不会检查类型)

    // 错误示例:不同类型比较会被编译器拒绝
    max(10, 3.14);  // ❌ 编译错误:T不能同时是int和double
    
    复制代码
  3. 广泛应用 :STL容器(如vector<T>/map<K,V>)都是模板类

    std::vector<int> vi;      // 整型容器
    std::vector<std::string> vs; // 字符串容器
    
    复制代码

对比传统方法

假设没有模板,要实现相同功能需要:

int maxInt(int a, int b) { /*...*/ }
double maxDouble(double a, double b) { /*...*/ }
char maxChar(char a, char b) { /*...*/ }
// 要写几十个类似的函数...
复制代码

有了模板就像拥有了代码复印机,只需描述通用逻辑,复印机会自动生成各种类型的版本。


为什么需要模板?

当你要实现逻辑相同但数据类型不同的功能时(比如各种数学计算、容器操作),模板能让你:

  • ✅ 只写一次代码

  • ✅ 避免复制粘贴错误

  • ✅ 更容易维护升级

模板类

同样,您可以使用关键字创建模板类。下面是一个表示一对值的简单模板类的示例:template

astro-code 复制代码
template <typename T1, typename T2>
class Pair {
public:
    T1 first;
    T2 second;

    Pair(T1 first, T2 second) : first(first), second(second) {}
};

要使用这个类,你需要在创建对象时指定类型参数:

astro-code 复制代码
Pair<int, std::string> pair(1, "Hello");

我们可以用多功能收纳盒来理解这个模板类:


代码作用

创建一个能装两种不同类型物品的包装盒,比如:

  • 左边格子放钥匙,右边格子放便签 ✉️🗝️

  • 左边放金额,右边放订单号 💰📦

  • 任何两种不同类型的组合都能装


拆解分析

  1. 定义模具规格

    template <typename T1, typename T2>  // 声明两个任意类型
    
    复制代码
    • 相当于说:"我要做一个包装盒,但暂时不确定装什么类型的东西,用T1表示第一个格子类型,T2表示第二个格子类型"
  2. 制作盒子结构

    class Pair {
    public:
        T1 first;  // 第一个格子(类型由T1决定)
        T2 second; // 第二个格子(类型由T2决定)
    
    复制代码
    • 盒子做好两个槽位:第一个槽位形状适配T1类型物品,第二个适配T2
  3. 安装装盒装置

    Pair(T1 first, T2 second) 
        : first(first), second(second) {}
    
    复制代码
    • 相当于一个装盒器:你递给他两个物品,自动按顺序放入对应格子

使用示例

// 装数字和字符串的盒子
Pair<int, std::string> idCard(12345, "张三"); 
// 现在盒子里:
// first = 12345 (int)
// second = "张三" (string)

// 装价格和货币单位的盒子
Pair<double, char> price(99.9, '¥');
// 现在盒子里:
// first = 99.9 (double)
// second = '¥' (char)
复制代码

对比普通类

如果不用模板,要实现相同功能需要:

// 只能装int和string的版本
class IntStringPair {
    int first;
    string second;
    //...构造函数
};

// 再写一个装double和char的版本
class DoubleCharPair {
    double first;
    char second;
    //...构造函数
};
// 每来一种新组合就要新写一个类 😫
复制代码

用了模板就像拥有自动模具机,只需说:"我要装A类型和B类型的盒子",机器立即生成对应的盒子。


现实应用场景

这种设计模式广泛用于:

  1. 返回多个值的函数(比如同时返回计算结果和状态码)

  2. 字典的键值对(std::pair就是标准库的类似实现)

  3. 坐标点(x和y可以是int/double等不同类型)

  4. 任何需要捆绑两个相关数据的场景


关键优势

  1. 类型自由组合:不再受限于固定类型搭配

  2. 代码零重复:一套代码适配所有类型组合

  3. 编译时检查:如果装错类型会立即报错(比如试图把字符串装进声明为int的格子)

我们可以用定制礼盒来理解模板特化的概念:


基础理解

想象你开了一家礼品包装店,大部分顾客都用通用礼盒(普通模板类):

template<typename T1, typename T2>
class Pair { /*...*/ }; // 普通礼盒,原样包装物品
复制代码

但有一天,有个客户要求:"当包装两个字母时,请自动把字母变成大写 "。这就是模板特化的场景。


特化过程解析

  1. 声明特制礼盒

    template<>  // 空尖括号表示这是特化版本
    class Pair<char, char> { // 明确指定两个类型都是char
    public:
        char first;
        char second;
    
    复制代码
    • 相当于在店里挂出告示:"特别说明:当且仅当包装两个字母时,启用特殊处理流程"
  2. 添加特殊处理

    Pair(char first, char second) 
        : first(std::toupper(first)),  // 转大写
          second(std::toupper(second)) {}
    
    复制代码
    • 相当于收到字母后:

      ① 先通过字母转换器std::toupper)处理

      ② 再放入礼盒


使用对比

// 普通礼盒(使用通用模板)
Pair<int, char> normalBox(97, 'b'); 
// 内容原样存储 → first=97, second='b'

// 特制礼盒(自动触发特化版本)
Pair<char, char> specialBox('a', 'b'); 
// 内容被转换 → first='A', second='B'
复制代码

特化的核心特点

  1. 精确匹配

    只有当模板参数完全符合 <char, char>时才会触发特化版本,其他情况仍用普通模板:

    Pair<char, int> box1('a', 98);  // 使用普通模板
    Pair<int, char> box2(97, 'b');  // 使用普通模板
    Pair<char, char> box3('a','b'); // 使用特化版本 ✅
    
    复制代码
  2. 独立实现

    特化版本需要完全重新实现类(不能只修改部分功能),就像重新设计一款新型号的礼盒。

  3. 优先级机制

    编译器会优先选择最匹配的版本,就像顾客说要"红色方形礼盒"时,店员会优先找特制的红色方形盒,而不是通用礼盒。


现实应用场景

这种技术常用于:

  1. 类型特殊处理

    • char类型进行大小写转换

    • 对指针类型进行空指针检查

    • 对字符串类型进行编码转换

  2. 性能优化

    对特定类型(如bool)采用更高效的内存布局

  3. 边界情况处理

    例如数学库中对整数/浮点数的不同计算方式


类比扩展

假设你的礼品店新增业务:

  • 普通礼盒:直接包装商品(通用模板)

  • 易碎品礼盒 :特化版本,内置防震材料(Pair<Glass, Glass>

  • 生鲜礼盒 :特化版本,附带冰袋(Pair<Food, Food>

每个特化版本都针对特定商品类型提供额外服务,但通用流程保持不变。


通过这种机制,C++模板既保持了通用性,又能灵活应对特殊需求。

可变参数模板

可变参数模板是 C++11 中的一项功能,允许您定义具有可变数量参数的模板。当您需要编写可以接受不同数量和类型参数的函数或类时,这尤其有用。

语法

可变参数模板的语法非常简单。要定义可变参数模板,请使用 (ellipsis) 表示法:...

astro-code 复制代码
template <typename... Args>

此表示法表示一个参数包,它可以包含零个或多个参数。您可以将此参数包用作模板定义中的模板参数的变量列表。

例子

使用可变参数模板对多个参数求和

astro-code 复制代码
#include <iostream>

// Base case for recursion
template <typename T>
T sum(T t) {
  return t;
}

// Variadic template
template <typename T, typename... Args>
T sum(T t, Args... args) {
  return t + sum(args...);
}

int main() {
  int result = sum(1, 2, 3, 4, 5);  // expands to 1 + 2 + 3 + 4 + 5
  std::cout << "The sum is: " << result << std::endl;

  return 0;
}

递归求和:像拆快递一样,每次拆开一个包裹取出数字,直到拆完所有包裹,最后把所有数字加起来。


分步解释

  1. 准备包裹堆

    sum(1, 2, 3, 4, 5); // 5个包裹摆在地上
    
    复制代码
  2. 拆包裹规则

    • 规则1(最终包裹):如果只剩最后一个包裹,直接取出里面的数字

      template <typename T>
      T sum(T t) { return t; } // 直接返回最后一个数字
      
      复制代码
    • 规则2(多个包裹):拆开第一个包裹,剩下的包裹继续拆

      return t + sum(args...); // 当前数字 + 剩余包裹的总和
      
      复制代码

拆解过程演示(以sum(1,2,3,4,5)为例)

拆解步骤 当前处理的数字 剩余包裹 计算表达式
第1步 1 [2,3,4,5] 1 + sum(2,3,4,5)
第2步 2 [3,4,5] 2 + sum(3,4,5)
第3步 3 [4,5] 3 + sum(4,5)
第4步 4 [5] 4 + sum(5)
第5步 5 5(触发终止条件)

最终计算:1 + (2 + (3 + (4 + 5))) = 15


关键技术点

  1. 可变参数模板 (typename... Args):

    • 类似可以装任意数量包裹的魔法袋子

    • args... 表示展开剩余的所有包裹

  2. 递归终止条件

    • 当包裹只剩1个时触发基例函数

    • 防止无限递归

  3. 编译时展开

    • 实际上编译器会生成5个不同版本的sum函数

    • 最终生成的代码相当于直接写 1+2+3+4+5


类比扩展

想象你在吃一串糖葫芦:

  1. 你每次咬下最前面的那颗(处理第一个参数)

  2. 把剩下的糖葫芦递给朋友用同样的方法吃(递归调用)

  3. 当只剩最后一颗时直接吃掉(终止条件)

  4. 最后统计总共吃了多少颗(求和结果)


输出结果

The sum is: 15

注意事项

如果传入不同类型(如sum(1, 2.5, 3)),返回类型由第一个参数类型决定(本例会丢失小数部分)。要处理这种情况需要更复杂的模板设计,但当前版本已满足基本整型求和需求。

使用可变参数模板的 Tuple 类

astro-code 复制代码
template <typename... Types>
class Tuple;

// Base case: empty tuple
template <>
class Tuple<> {};

// Recursive case: Tuple with one or more elements
template <typename Head, typename... Tail>
class Tuple<Head, Tail...> : public Tuple<Tail...> {
 public:
  Tuple(Head head, Tail... tail) : Tuple<Tail...>(tail...), head_(head) {}

  Head head() const { return head_; }

 private:
  Head head_;
};

int main() {
  Tuple<int, float, double> tuple(1, 2.0f, 3.0);
  std::cout << "First element: " << tuple.head() << std::endl;
  return 0;
}

请注意,显示的示例用于教育目的,可能不是最有效或可用于生产的实施。在 C++17 及更高版本中,有更简洁的方法可以处理可变参数模板,例如使用 fold 表达式。

我们可以用俄罗斯套娃来比喻这个元组(Tuple)的实现原理:


代码功能

实现一个能存储多个不同类型值的容器,像一组逐渐变小的套娃,每个套娃只负责保管自己那一层的物品,同时知道下一层套娃的位置。


核心原理拆解

  1. 空套娃(终止条件)

    template <> class Tuple<> {}; // 最内层的空套娃
    
    复制代码
    • 这是递归的终点,相当于最小的那个不能再打开的套娃
  2. 套娃制造规则

    template <typename Head, typename... Tail>
    class Tuple<Head, Tail...> : public Tuple<Tail...> { // 继承更小的套娃
    private:
        Head head_;  // 当前层保管的物品
    public:
        Tuple(Head head, Tail... tail) 
            : Tuple<Tail...>(tail...), // 先把剩下的物品装进更小的套娃
              head_(head) {}           // 自己保管第一个物品
    };
    
    复制代码
    • 每个套娃结构:

      🪆【当前物品】+ 🔗【更小的套娃】


创建过程演示(以Tuple<int, float, double>(1, 2.0f, 3.0)为例)

套娃层级 保管的物品 内部包含的下一层套娃
第1层 int 1 Tuple<float, double>(2.0f, 3.0)
第2层 float 2.0 Tuple<double>(3.0)
第3层 double 3.0 Tuple<>(空套娃)

实际内存结构类似:

[ head_=1 ] → [ head_=2.0 ] → [ head_=3.0 ] → [ 空 ]
复制代码

访问第一个元素

tuple.head(); // 直接取最外层套娃自己保管的物品 → 1
复制代码

关键技术点

  1. 递归继承:通过继承关系把长参数列表逐层拆解

    • Tuple<A,B,C>继承自Tuple<B,C>

    • Tuple<B,C>继承自Tuple<C>

    • Tuple<C>继承自Tuple<>

  2. 编译时展开:编译器会根据模板参数自动生成所有层级的套娃类

  3. 类型安全存储 :每个层级的head_类型都是确定的(比如第一层是int,第二层是float)


类比扩展

想象你要给不同年级的学生传递秘密消息:

  1. 把消息拆成三部分:文字(给高中生)、公式(给初中生)、图画(给小学生)

  2. 高中生拿到文字后,把剩下的传给初中生

  3. 初中生拿到公式后,把剩下的传给小学生

  4. 小学生最后拿到图画

每个学生只处理自己那部分信息,但通过传递链最终完成完整信息的传递。


输出结果

First element: 1
复制代码

后续扩展方向

若要访问其他元素(如第二个元素),可以通过类型转换访问基类:

// 获取第二个元素(需要强制类型转换)
auto& base = static_cast<Tuple<float, double>&>(tuple);
std::cout << "Second element: " << base.head(); // 输出2
复制代码

通过这种递归继承的方式,用极简的代码实现了类型安全的多元组存储,这正是C++标准库中std::tuple的基础实现原理(当然标准库的实现更复杂高效)。

在C++中,冒号 : 在不同上下文中有不同的含义。让我们用搭积木的比喻来拆解这段代码中所有冒号的作用:


代码中的冒号主要出现在两个地方

1. 类继承时的冒号(搭积木的基础)
class Tuple<Head, Tail...> : public Tuple<Tail...> {
//                          ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
复制代码
  • 作用 :表示继承关系,相当于说:"这个积木块要搭在另一个积木块上"

  • 类比

    想象你要搭一个三层积木塔:

    • 最底层积木:Tuple<>(空基类)

    • 中间层积木:Tuple<double> 继承自 Tuple<>

    • 顶层积木:Tuple<float, double> 继承自 Tuple<double>

    • 最顶层:Tuple<int, float, double> 继承自 Tuple<float, double>

  • 实际效果

    每个派生类会自动拥有基类的所有成员(但本例中基类只有构造函数)


2. 构造函数后的冒号(组装积木的说明书)
Tuple(Head head, Tail... tail) 
    : Tuple<Tail...>(tail...), // 先组装下层积木
      head_(head)             // 再安装当前层的零件
{}
//↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
复制代码
  • 作用成员初始化列表,用于:

    1. 调用基类构造函数(组装下层积木)

    2. 初始化成员变量(安装当前积木的零件)

  • 分步解释

    • : Tuple<Tail...>(tail...)

      → 先让基类(下层积木)用tail...参数完成构造

      → 相当于先搭好下面的积木层

    • head_(head)

      → 再用head参数初始化当前层的head_成员

      → 相当于在当前层放上特定零件

  • 为什么重要

    如果不这样做:

    • 基类可能无法正确初始化(下层积木没搭好)

    • 成员变量会是随机值(零件没安装)


对比现实场景

假设你要组装一台电脑:

class GamingPC : public Computer { // 继承自基础电脑
public:
    GamingPC(CPU cpu, GPU gpu) 
        : Computer(cpu),  // 先组装基础电脑(用CPU)
          gpu_(gpu) {}     // 再安装独立显卡
private:
    GPU gpu_;
};
复制代码
  • : Computer(cpu) → 先装好基础电脑

  • gpu_(gpu) → 再装显卡


其他潜在冒号用法(本代码未涉及但常见)

  1. 访问权限声明

    class MyClass {
    public:   // ← 冒号表示后续成员是公开的
      int a;
    private:  // ← 冒号表示后续成员是私有的
      int b;
    };
    
    复制代码
  2. 位域定义

    struct Flags {
      unsigned int flag1 : 1;  // 用1个二进制位存储flag1
      unsigned int flag2 : 3;  // 用3个二进制位存储flag2
    };
    
    复制代码
  3. 三目运算符

    int x = (a > b) ? a : b; // ← 条件 ? 结果1 : 结果2
    
    复制代码

回到当前代码的关键点

当看到冒号时,立刻问自己:

  1. 出现在类名后 → 继承关系(搭积木的基础)

  2. 出现在构造函数后 → 初始化列表(组装说明书)

  3. 出现在访问修饰符后 → 权限控制(公开/私有区域分界)

这样就能快速理解代码的组织结构,就像看懂积木塔的搭建蓝图一样清晰!

我们可以用快递拆箱过程 来理解这些语法符号的运作方式,特别是省略号(...)的作用:


核心概念:参数包展开

代码中的 ... 就像自动拆箱机 ,用来处理不确定数量的类型或值 。关键要看 ... 出现的位置:


1. 模板参数声明(准备快递箱清单)
template <typename... Types>  // 👉 声明一个类型参数包
class Tuple;
复制代码
  • typename... Types 表示:

    • "我要处理多个未知类型 ,这些类型被打包成一个叫Types的包裹"

    • 类似快递员拿到一个未拆封的大箱子,里面可能有任意数量和类型的物品


2. 模板参数分割(拆开最外层快递箱)
template <typename Head, typename... Tail>
//          ↑第一个类型    ↑剩余类型组成的包裹
class Tuple<Head, Tail...> // 👉 用拆出来的参数实例化模板
复制代码
  • 类比拆箱过程

    1. 拿到一个大箱子 Types = int, float, double

    2. 拆出第一个物品 Head = int

    3. 剩下的物品重新打包成 Tail = float, double

  • 语法规则

    • typename... Tail:声明Tail是一个类型参数包

    • Tail...展开参数包(把包裹里的内容取出来)


3. 递归继承(传递剩余包裹)
: public Tuple<Tail...>  // 👉 把剩下的包裹传给下一层
复制代码
  • 继续拆箱

    • 当前层处理完Head后,把Tail包裹传给基类

    • 基类 Tuple<Tail...> 会继续拆解成 Tuple<float, double>

    • 这个过程持续到包裹为空


4. 构造函数参数处理(逐层处理物品)
Tuple(Head head, Tail... tail)  // 👉 拆解值参数包
    : Tuple<Tail...>(tail...),  // 👉 传递剩余值
      head_(head) {}
复制代码
  • 值参数包展开

    • Tail... tail:声明函数参数包,可接受多个值

    • tail...:展开值参数包,把剩余值传递给基类构造函数


完整拆解演示(以Tuple<int, float, double>为例)

阶段1:模板实例化
实例化层级 当前处理的类型参数 剩余类型参数包
第1层 Head = int Tail... = float, double
第2层 Head = float Tail... = double
第3层 Head = double Tail... = 空
阶段2:构造函数调用
Tuple<int, float, double>(1, 2.0f, 3.0)
→ 调用基类构造函数:Tuple<float, double>(2.0f, 3.0)
   → 调用基类构造函数:Tuple<double>(3.0)
      → 调用基类构造函数:Tuple<>()
复制代码

省略号三定律

  1. 声明参数包 :在类型前加 typename...(例:typename... Tail

  2. 使用参数包 :在已声明的参数包后加 ... 展开(例:Tail...

  3. 保持位置同步:类型参数包和值参数包的展开位置要对应


类比现实场景

想象你在处理一组嵌套的快递:

  1. 拿到大箱子 📦[📦A, 📦B, 📦C]

  2. 拆出第一个箱子A,剩下的 📦[📦B, 📦C] 传给助手

  3. 助手拆出B,剩下的 📦C 传给另一个助手

  4. 最后一个助手拆出C,剩下空箱子

每个...就相当于说:"把剩下的箱子交给下一个处理者"。


对比普通模板

普通模板:

template <typename T1, typename T2> // 只能处理固定两个类型
class Pair { /*...*/ };
复制代码

变参模板:

template <typename... Types> // 处理任意数量类型
class Tuple { /*...*/ };
复制代码

通过这种设计,编译器会在编译时自动展开所有参数包,最终生成类似手工编写的多层嵌套类。这正是C++元编程的核心魅力:用简洁的语法描述复杂的类型结构

我们可以用多功能旅行箱 来理解这个 Tuple 对象的作用:


元组(Tuple)的核心作用

安全地打包多个不同类型的值,像一个智能旅行箱:

  1. 每个隔层只能放特定类型的物品(如第一层放护照,第二层放现金,第三层放充电器)

  2. 物品按顺序存放不会混淆类型

  3. 随时可取出指定隔层的物品


你代码中元组的具体表现

1. 分层存储(像俄罗斯套娃)

当创建 Tuple<int, float, double> tuple(1, 2.0f, 3.0) 时:

旅行箱结构:
[最外层] → int 1
  ↓
[中层] → float 2.0
  ↓
[最内层] → double 3.0
复制代码
2. 类型安全(智能安检系统)
  • 如果试图把字符串放进声明为 int 的隔层 → 编译报错(类似安检发现违禁品)

  • 如果取出时用错类型 → 编译报错(类似用钥匙开错了储物柜)

3. 顺序固定(隔层编号不可变)
  • 第一个元素永远是 int 类型(类似旅行箱的第一层永远是证件层)

  • 元素顺序在创建时确定后不可更改


对比现实场景

假设你要管理一个跨城搬家车队

  • 普通数组 → 所有卡车只能装同一种货物(如全是家具)

  • 元组 → 头车装文件(string),第二辆车装易碎品(Glass),第三辆车装植物(Plant),各车货物类型不同但整体形成运输组合


你代码的特别之处

当前实现特点:
  1. 只能取第一个元素head() 方法)→ 类似旅行箱只能直接打开最外层

  2. 后续元素需要拆解基类访问 → 要打开中层需要先拆开外层

  3. 递归继承实现 → 像用套娃式的包装确保每层类型独立

对比标准库的 std::tuple
  • 标准库元组更像智能分格行李箱 ,可以直接通过 get<0>() get<1>() 访问任意位置

  • 你的实现展示了元组最基础的核心原理(实际标准库实现更复杂高效)


实际应用场景

  1. 函数返回多个值

    Tuple<bool, string> 登录验证() {
        return Tuple(true, "成功");
    }
    
    复制代码
  2. 临时组合数据

    Tuple<string, int> 学生档案("张三", 18);
    
    复制代码
  3. 替代结构体

    // 无需先定义结构体,直接打包数据
    Tuple<string, double, Date> 订单("手机", 2999.9, Date(2023,12,1));
    关键总结
    
    复制代码

关键总结

这个 Tuple 就像一个类型安全的魔法压缩包

  • ✅ 把零散数据打包成单一对象

  • ✅ 保持内部元素的类型特征

  • ✅ 通过编译检查确保操作安全

  • ❌ 当前版本功能较基础(实际开发建议直接用 std::tuple

模板专业化

模板专用化是一种为特定类型或一组类型自定义或修改模板行为的方法。当您想要优化行为或为特定类型提供特定实现,而不影响其他类型的模板的整体行为时,这可能很有用。

您可以通过两种主要方式来专用化模板:

  • **完全专业化:**当您为特定类型或一组类型提供特定实现时,会发生这种情况。

  • **部分专业化:**当您为与特定模式或条件匹配的类型子集提供更通用的实现时,会发生这种情况。

完整模板专业化

当您想要为特定类型创建模板的单独实现时,将使用完全专用化。为此,您需要使用 keyword,后跟具有所需专用类型的函数模板。template<>

下面是一个示例:

#include <iostream>

template <typename T>
void printData(const T& data) {
    std::cout << "General template: " << data << std::endl;
}

template <>
void printData(const char* const & data) {
    std::cout << "Specialized template for const char*: " << data << std::endl;
}

int main() {
    int a = 5;
    const char* str = "Hello, world!";
    printData(a); // General template: 5
    printData(str); // Specialized template for const char*: Hello, world!
}

我们可以用定制蛋糕模具来比喻模板专用化的概念:


模板专用化是什么?

想象你开了一家蛋糕店,大部分顾客用通用模具做圆形蛋糕(普通模板)。但有些VIP客户要求:

  • VIP客户A:"我要专门给草莓蛋糕设计心形模具!"

  • VIP客户B:"所有水果蛋糕都要加防粘层处理!"

模板专用化 就是根据特殊需求定制模具,让同一套模板代码对特定类型有不同的处理方式。


两种定制方式对比

1️⃣ 完全专用化(Full Specialization) → 专属私人订制
  • 场景 :只为特定类型组合打造专属模具

  • 示例

    // 通用模具:适合所有水果
    template<typename T>
    class Cake { /* 圆形模具 */ };
    
    // 完全专用化:只给草莓蛋糕用心形模具
    template<>
    class Cake<Strawberry> { /* 心形模具 */ };
    
    复制代码
  • 特点

    • 必须明确指定所有模板参数 (如Strawberry

    • 就像VIP客户指定:"只要草莓味的用这个模具,其他不变"

2️⃣ 部分专用化(Partial Specialization) → 批量定制服务
  • 场景 :为某一类类型设计特殊模具

  • 示例

    // 通用模具:适合所有水果
    template<typename T>
    class Cake { /* 圆形模具 */ };
    
    // 部分专用化:所有冰冻水果用方形模具
    template<typename T>
    class Cake<Frozen<T>> { /* 方形模具 */ };
    
    复制代码
  • 特点

    • 通过模式匹配 确定适用类型(如所有Frozen<>包装的类型)

    • 就像VIP客户要求:"所有冰冻过的水果都要特殊处理"


现实代码案例

案例1:完全专用化(字符串特殊处理)
// 通用比较函数
template<typename T>
bool isEqual(T a, T b) { return a == b; }

// 完全专用化:C风格字符串比较
template<>
bool isEqual<char*>(char* a, char* b) {
    return strcmp(a, b) == 0;
}
复制代码
案例2:部分专用化(指针通用处理)
// 通用打印函数
template<typename T>
void print(T val) { cout << val; }

// 部分专用化:所有指针类型用这个版本
template<typename T>
void print<T*>(T* val) { cout << "指针地址:" << val; }
复制代码

为什么要用专用化?

场景 目的 类比
性能优化 针对特定类型优化计算方式 为巧克力设计防融化模具
特殊逻辑 处理类型特有的行为 为冰淇淋蛋糕加干冰层
修复默认行为 修正模板对某些类型的不当处理 修正慕斯蛋糕容易塌陷的问题
扩展功能 为特定类型添加额外功能 给生日蛋糕加蜡烛插槽

使用时机判断

当遇到以下情况时考虑专用化:

  1. 该类型表现特殊 :比如char*需要strcmp而不是==

  2. 性能瓶颈 :比如对bool类型采用位压缩存储

  3. 添加元数据:比如给所有指针类型添加引用计数

  4. 类型限制:比如禁止某些类型使用模板


语法记忆口诀

template</* 空 */>   // 完全专用化要清空模板参数
class Name<具体类型> // 明确指定特殊类型

template<剩余参数>   // 部分专用化保留部分参数
class Name<模式匹配> // 用<T*> <vector<T>>等模式
复制代码

通过这种定制化设计,既能保持模板的通用性,又能精准处理特殊需求,就像米其林餐厅既提供标准菜单,也能根据客人喜好定制料理。

完整模板专业化

完全模板专用化允许您在与一组特定类型参数一起使用时为模板提供特定的实现或行为。当您想要处理特殊情况或针对特定类型优化代码时,它非常有用。

语法

要创建模板的完整特化,您需要定义应进行特化的特定类型。语法如下所示:

astro-code 复制代码
template <> //Indicates that this is a specialization
className<specificType> //The specialized class for the specific type

请考虑以下示例来演示完整的模板专用化:

astro-code 复制代码
// Generic template
template <typename T>
class MyContainer {
public:
    void print() {
        std::cout << "Generic container." << std::endl;
    }
};

// Full template specialization for int
template <>
class MyContainer<int> {
public:
    void print() {
        std::cout << "Container for integers." << std::endl;
    }
};

int main() {
    MyContainer<double> d;
    MyContainer<int> i;

    d.print(); // Output: Generic container.
    i.print(); // Output: Container for integers.

    return 0;
}

在此示例中,我们定义了一个泛型 template 类以及 type 的完整特化。当我们使用具有该类型的容器时,将调用专用实现的方法。对于其他类型,将使用泛型模板实现。MyContainer ``int ``int ``print

MyTemplate<double*> t2;

匹配过程

  1. double* 是一个指针类型,所以编译器不会匹配 MyTemplate<int> (因为它只匹配 int)。
  2. double* 符合 MyTemplate<T*> 这个部分特化模板 ,其中 T = double
  3. 于是 MyTemplate<T*> 变成 MyTemplate<double*>t2 这个对象会调用这个特化模板的 name() 方法。

类型 Traits

类型特征是 C++ 中的一组模板类,可帮助获取有关类型的属性、行为或特征的信息。可以在头文件中找到它们。通过使用 Type Traits,您可以根据给定类型的属性调整代码,甚至可以在模板代码中为类型参数强制执行特定属性。<type_traits>

一些常见的类型特征是:

  • std::is_pointer:检查给定类型是否为指针类型。
  • std::is_arithmetic:检查给定类型是否为算术类型。
  • std::is_function:检查给定类型是否为函数类型。
  • std::decay:将 decltype 规则应用于输入类型( strips 引用、cv 限定符等)。

用法

您可以使用这样的类型 traits:

#include <iostream>
#include <type_traits>

int main() {
    int a;
    int* a_ptr = &a;

    std::cout << "Is 'a' a pointer? " << std::boolalpha << std::is_pointer<decltype(a)>::value << std::endl;
    std::cout << "Is 'a_ptr' a pointer? " << std::boolalpha << std::is_pointer<decltype(a_ptr)>::value << std::endl;

    return 0;
}

类型 Traits 是 C++ 中用来探测类型特性 的工具,就像给类型做"体检报告"。它们藏在 <type_traits> 头文件里,能帮你回答这些问题:


核心作用

  1. 检查类型属性(像医生体检):

    • 是指针吗?std::is_pointer<T> 👉 查 T 是否指针(如 int*

    • 是数字吗?std::is_arithmetic<T> 👉 查 T 是否是 intfloat 等数值类型

    • 是函数吗?std::is_function<T> 👉 查 T 是否是函数(如 void(int)

  2. 改造类型(像美颜滤镜):

    • std::decay<T> 👉 去掉引用、constvolatile 修饰,如果是数组或函数,退化成指针(就像函数传参时的类型转换)

通俗比喻

想象你写了一个万能模板函数,但不同食材需要不同处理方式

  • 如果是指针 (如 int*),你要特别处理(比如解引用)

  • 如果是数字 (如 double),你要做数学运算

  • 如果是函数 (如 void()),你要调用它

类型 Traits 就是帮你自动识别这些"食材特性"的工具,让模板代码能"对症下药"。


具体用法示例

#include <type_traits>

template<typename T>
void Process(T value) {
    // 检查 T 是不是指针
    if constexpr (std::is_pointer_v<T>) {
        // 对指针的特殊处理(如解引用)
        *value = 42;
    } else if constexpr (std::is_arithmetic_v<T>) {
        // 对数字的特殊处理(如相加)
        T result = value + 100;
    }
}

// 强制类型必须是数字(编译时报错)
template<typename T>
void SafeMath(T value) {
    static_assert(std::is_arithmetic_v<T>, "必须用数字类型!");
    // ...
}#include <type_traits>

template<typename T>
void Process(T value) {
    // 检查 T 是不是指针
    if constexpr (std::is_pointer_v<T>) {
        // 对指针的特殊处理(如解引用)
        *value = 42;
    } else if constexpr (std::is_arithmetic_v<T>) {
        // 对数字的特殊处理(如相加)
        T result = value + 100;
    }
}

// 强制类型必须是数字(编译时报错)
template<typename T>
void SafeMath(T value) {
    static_assert(std::is_arithmetic_v<T>, "必须用数字类型!");
    // ...
}
复制代码

为什么要用?

  • 模板编程更灵活:根据类型特性走不同分支(如指针 vs 非指针)

  • 编译时安全检查:提前拦截非法类型(比如禁止用非数字类型做数学运算)

  • 代码更简洁:避免手写复杂的类型判断逻辑


一句话总结

类型 Traits 是 C++ 模板的"侦探工具",帮你摸清类型的底细,让通用代码更智能、更安全。

组合类型 trait

某些类型特征可帮助您组合其他特征或修改它们,例如:

  • std::conditional:如果给定的布尔值为 true,则使用类型 A;否则,请使用 Type B。
  • std::enable_if:如果给定的布尔值为 true,则使用类型 A;否则,没有嵌套类型。
astro-code 复制代码
#include <iostream>
#include <type_traits>

template <typename T>
typename std::enable_if<std::is_arithmetic<T>::value, T>::type find_max(T a, T b) {
    return a > b ? a : b;
}

int main() {
    int max = find_max(10, 20);
    std::cout << "Max: " << max << std::endl;

    return 0;
}

在此示例中,仅当 T 为算术类型(例如 int、float、double)时,才定义模板函数。这可以防止意外地将函数与非算术类型一起使用。find_max``find_max

总体而言,类型特征是创建更通用、可扩展和高效的 C++ 代码的强大工具,提供了一种根据类型特征查询和调整代码的方法。

这段代码实现了一个**"智能取最大值"的函数**,并且只允许数字类型(如整数、浮点数)使用它。如果尝试用非数字类型(比如字符串),代码会在编译时报错,无法通过。以下是通俗解释:


代码拆解

1. 模板函数的"安检门"
template <typename T>
typename std::enable_if<std::is_arithmetic<T>::value, T>::type 
find_max(T a, T b) {
    return a > b ? a : b;
}
复制代码
  • std::is_arithmetic<T> :检查 T 是否是数字类型(比如 intdouble 等)。

  • std::enable_if<条件, 返回类型>

    • 如果条件满足(T 是数字),函数返回 T 类型,正常使用。

    • 如果条件不满足(比如 T 是字符串),这个函数模板会被"隐藏",编译时会报错。

通俗比喻

这个函数就像一个"只允许数字进入的VIP通道"。如果你拿着数字(比如 1020),保安(编译器)放行;如果你拿的是其他东西(比如 "苹果""香蕉"),保安直接拦住。


2. 主函数调用
int main() {
    int max = find_max(10, 20);  // T 被推导为 int
    std::cout << "Max: " << max << std::endl;
    return 0;
}
复制代码
  • find_max(10, 20) :这里 T 自动推断为 int,符合数字类型条件,函数返回 20

  • 输出结果Max: 20


如果乱用会发生什么?

假设你调皮地写:

find_max("hello", "world");  // T 被推导为 const char*
复制代码
  • 编译器会报错 :因为 const char*(字符串)不是算术类型,函数模板被"隐藏",找不到可用函数。

关键设计思想

  1. 编译时安全检查:在代码编译阶段就拦截非法类型,避免运行时出现不可预测的错误。

  2. 模板的"选择性启用" :通过 std::enable_if,只有符合条件的类型才能调用这个函数。

  3. 代码简洁性:无需手写类型判断逻辑,直接利用标准库工具实现限制。


类比场景

想象你开了一个"数学运算超市",但规定只卖数字相关的商品:

  • find_max 是你的"比大小机"。

  • std::is_arithmetic 是门口的扫码器,检查顾客带的是否是数字。

  • std::enable_if 是门禁系统:扫码通过就开门,否则报警。


一句话总结

这段代码是"类型安全的取最大值函数"------只认数字,其他类型一律拒之门外!🔒

这段代码用到了 C++ 模板编程中的两个重要概念:类型检查条件编译。我把它拆解成几个关键点,用生活场景类比解释:


1. std::is_arithmetic<T>::value ------ "安检门"

  • 作用 :检查类型 T 是否是算术类型 (比如 intfloatdouble 等数字类型)。

  • 通俗解释

    像一个超市门口的安检机,如果顾客带的是食物(数字类型),就放行;如果是危险品(非数字类型,比如字符串),就触发警报。


2. std::enable_if<条件, 返回类型> ------ "开关控制器"

  • 作用:只有条件为真时,才允许这个函数模板存在;否则直接"隐藏"它。

  • 通俗解释

    像一个智能电灯开关:

    • 如果条件满足(比如检测到有人进入房间),开关打开,灯亮(函数可用)。

    • 如果条件不满足(比如房间没人),开关关闭,灯灭(函数不存在)。


3. typename 的作用 ------ "解谜工具"

  • 问题std::enable_if<...>::type 中的 type 是依赖于模板参数 T 的类型,编译器无法直接识别。

  • typename 的作用 :告诉编译器 std::enable_if<...>::type 是一个类型(而不是变量或其他东西)。

  • 通俗解释

    像告诉盲人:"你手里拿的是一个杯子(类型),不是石头(变量)"。


4. 整段代码的协作流程

template <typename T>
typename std::enable_if<std::is_arithmetic<T>::value, T>::type 
find_max(T a, T b) {
    return a > b ? a : b;
}
复制代码
  • 步骤拆解

    1. 检查类型std::is_arithmetic<T> 判断 T 是不是数字类型。

    2. 条件编译 :如果检查通过(valuetrue),std::enable_if 会生成返回类型 T,函数正常存在。

    3. 隐藏函数 :如果检查不通过(比如 Tstring),std::enable_if 不生成返回类型,这个函数模板会被"隐藏",编译器会报错"找不到函数"。


5. 如果去掉 std::enable_if 会怎样?

  • 代码会变成普通的模板函数:

    template <typename T>
    T find_max(T a, T b) { return a > b ? a : b; }
    
    复制代码
  • 问题 :如果传入不支持 > 运算符的类型(比如两个学生对象),编译时会报错"operator> 未定义",错误提示不如 enable_if 直观。


6. 完整技术名称:SFINAE(替换失败不是错误)

  • 原理:当模板推导失败时,编译器不会报错,而是默默忽略这个候选函数。

  • 在这段代码中的应用 :如果 std::enable_if 的条件不满足,这个函数模板会被"静默丢弃",不会参与重载决议。


类比场景总结

想象你要设计一个只能计算数字的智能计算器:

  1. std::is_arithmetic 👉 检测用户输入的是否是数字。

  2. std::enable_if 👉 如果是数字,亮起"计算可用"绿灯;否则直接关闭计算功能。

  3. typename 👉 明确告诉计算器"你处理的是数字类型,不是其他奇怪的东西"。


为什么这样设计?

  • 安全:提前拦截非法类型,避免运行时崩溃。

  • 清晰:编译时报错信息更明确(比如直接提示"类型不是算术类型")。

  • 灵活:模板可以根据类型特性自动调整行为。

在 C++ 中,:: 这个符号叫做作用域解析运算符 ,可以理解为"从哪里找 "的标记。它的核心作用是明确告诉编译器:某个名字属于哪个作用域(比如命名空间、类、全局作用域等)。


常见用途和通俗解释:

1. 访问命名空间中的内容

比如 std::cout

  • std 是一个命名空间(像"姓氏")。

  • :: 表示"从 std 这个命名空间里找"。

  • cout 是这个命名空间中的对象(像"名字")。

  • 合起来就是:"我要用 std 家的 cout"。

类比

就像班级里有两个同名的"小明",老师会说"一班的小明"和"二班的小明"来区分。:: 的作用类似"班级名 + 小明"。


2. 访问类的静态成员或类型

比如 MyClass::staticValue

  • MyClass 是一个类名。

  • :: 表示"从 MyClass 这个类里找"。

  • staticValue 是这个类的静态成员变量(或函数)。

示例

class Math {
public:
    static const double PI; // 静态成员
};
// 使用时:
double circleArea = Math::PI * radius * radius;
复制代码

3. 访问全局作用域

如果局部变量和全局变量同名,可以用 :: 强制访问全局变量:

int x = 100; // 全局变量

void func() {
    int x = 10; // 局部变量
    std::cout << x;        // 输出局部变量 10
    std::cout << ::x;      // 输出全局变量 100(::前面没有名字,表示全局)
}
复制代码

. 的区别

  • . 用于对象实例的成员访问(比如 obj.member)。

  • :: 用于类名、命名空间等作用域本身 的访问(比如 ClassName::member)。


一句话总结

:: 就像代码中的"导航符号",告诉编译器:"去某某地方找某某东西"。

------ 避免名字冲突,明确指定来源!

在 C++ 中,::: 是两个完全不同的符号,用途和场景完全不同。它们的区别可以用日常场景类比:


::(双冒号)

  • 作用:导航符号,表示**"从哪里找"**。

  • 常见场景

    1. 访问命名空间:std::cout(从 std 命名空间找 cout)。

    2. 访问类的静态成员:MyClass::staticValue

    3. 访问全局变量:::globalVar(当局部变量和全局变量同名时)。

通俗比喻

像写地址时的"省/市/区",比如 中国::北京::天安门,告诉编译器具体的位置。


:(单冒号)

  • 作用:分隔符或标记符,表示**"接下来是某个操作"**。

  • 常见场景

    1. 继承

      class Child : public Parent { ... }; // Child 继承自 Parent
      
      复制代码

      类比:"孩子是父母的延伸"。

    2. 构造函数初始化列表

      class MyClass {
      public:
          MyClass(int x) : value(x) { ... } // 初始化成员变量 value
      };
      
      复制代码

      类比:"建房时先打地基,再盖楼"。

    3. 三目运算符

      int result = (a > b) ? a : b; // 如果 a > b 取 a,否则取 b
      
      复制代码

      类比:"二选一的分支"。

    4. 标签语法 (如 switchcase):

      switch (x) {
          case 1: ... // 当 x=1 时执行
          default: ... 
      }
      
      复制代码

对比总结

符号 用途 类比 例子
:: 明确作用域来源 导航路径 std::coutMyClass::func
: 分隔或标记操作 分隔符或分支标记 继承、初始化列表、三目运算符

一句话记忆

  • :: 是**"找东西的导航"**(明确来源)。

  • : 是**"分隔步骤或标记操作"**(划分动作或条件)。

SFINAE(替换失败不是错误)

SFINAE 是 C++ 模板元编程中的一个原则,它允许编译器在替换期间特定模板专用化失败时选择适当的函数或类。术语 "替换失败" 是指编译器尝试将模板参数替换为函数模板或类模板的过程。如果替换导致错误,编译器不会将该特定特化视为候选项,而是将继续搜索有效的特化。

SFINAE 背后的关键思想是,如果发生替换错误,则会以静默方式忽略该错误,编译器会继续探索其他模板专用化或重载。这允许您编写更灵活和通用的代码,因为它使您能够为不同的场景提供多个专业化。

代码示例

下面是一个演示 SFINAE 运行的示例:

#include <iostream>
#include <type_traits>

template <typename T, typename = void>
struct foo_impl {
    void operator()(T t) {
        std::cout << "Called when T is not arithmetic" << std::endl;
    }
};

template <typename T>
struct foo_impl<T, std::enable_if_t<std::is_arithmetic<T>::value>> {
    void operator()(T t) {
        std::cout << "Called when T is arithmetic" << std::endl;
    }
};

template <typename T>
void foo(T t) {
    foo_impl<T>()(t);
}

int main() {
    int a = 5;
    foo(a); // output: Called when T is arithmetic

    std::string s = "example";
    foo(s); // output: Called when T is not arithmetic
}

在此示例中,我们定义了两个函数,它们都是基于布尔值的 专用函数。第一个选项在 is a arithmetic type 时启用,而第二个参数在 is not a arithmetic type 时启用。然后,该函数根据 type trait 的结果调用适当的特化。foo_impl``std::is_arithmetic<T>``T``T``foo``foo_impl

使用整数调用时,将选择第一个专用化,使用字符串调用时,将选择第二个专用化。如果没有有效的专用化,则代码将无法编译。foo(a)``foo(s)

好的!我用一个点外卖的比喻来解释 SFINAE,保证你秒懂:


SFINAE 是什么?

想象你是一个编译器,用户写了一段代码说要"点外卖",而代码里可能有多个"餐馆选项"(函数模板或重载函数)。SFINAE 的规则就是:如果某个餐馆暂时关门(模板替换失败),你直接跳过它,继续找其他开门的餐馆,而不是直接报错说"外卖点不了"


具体步骤拆解:

  1. 用户点餐(调用函数)

    比如用户写了 func(10),编译器要找一个能处理 int 类型参数的 func 函数。

  2. 检查所有备选餐馆(候选函数模板)

    假设有两个候选:

    • 餐馆Atemplate<typename T> void func(T a)

      (普通模板,接受任何类型)

    • 餐馆Btemplate<typename T> void func(T* a)

      (只接受指针类型)

  3. 尝试"替换"参数(模板推导)

    • 对于 func(10)T 被推导为 int,餐馆B需要 T*(即 int*),但用户传的是 int替换失败

    • 根据 SFINAE 规则:餐馆B的替换失败不算错误,直接忽略它,继续用餐馆A。

  4. 最终结果

    成功调用餐馆A的 func<int>(10),用户吃到外卖!


SFINAE 的核心规则

  • 替换失败 (比如类型不匹配、表达式无效)不是错误,编译器会默默跳过这个选项。

  • 编译器会继续找其他能匹配的候选,直到找到唯一可行的选项,否则才报错。


代码示例:限制函数只接受数字类型

#include <type_traits>

// 餐馆A:只接受数字类型(SFINAE 控制)
template<typename T>
typename std::enable_if<std::is_arithmetic<T>::value, void>::type
func(T a) {
    // 处理数字...
}

// 餐馆B:其他类型报错(或处理非数字)
template<typename T>
typename std::enable_if<!std::is_arithmetic<T>::value, void>::type
func(T a) {
    static_assert(false, "必须传数字类型!");
}

int main() {
    func(10);     // 调用餐馆A
    func("hello");// 调用餐馆B,触发 static_assert 报错
}
复制代码
  • 如果传 10 :餐馆A的条件满足(std::is_arithmetic<int>::valuetrue),函数存在;餐馆B被跳过。

  • 如果传 "hello":餐馆A的条件不满足,函数被"隐藏";餐馆B被选中,触发静态断言报错。


为什么要用 SFINAE?

  • 灵活控制模板:根据类型特性选择不同实现(比如数字和非数字分开处理)。

  • 编译时安全:提前拦截无效类型,避免运行时崩溃。

  • 减少代码冗余:不用手写一堆特化版本,让编译器自动筛选。


一句话总结

SFINAE 就像编译器的"智能筛选器"------尝试所有选项,跳过坏的,留下好的,让模板代码既灵活又安全!

相关推荐
加油,旭杏几秒前
C++方向的面经
开发语言·c++
虾球xz14 分钟前
游戏引擎学习第137天
人工智能·学习·游戏引擎
王有品24 分钟前
python之爬虫入门实例
开发语言·爬虫·python
一水鉴天27 分钟前
为AI聊天工具添加一个知识系统 之135 详细设计之76 通用编程语言 之6
开发语言·人工智能·架构
m0_7482475539 分钟前
数据库系统架构与DBMS功能探微:现代信息时代数据管理的关键
java·开发语言·数据库
环能jvav大师1 小时前
Electron桌面应用开发:自定义菜单
开发语言·前端·javascript·windows·electron
一水鉴天1 小时前
为AI聊天工具添加一个知识系统 之136 详细设计之77 通用编程语言 之7
开发语言·人工智能·架构
一只小小汤圆1 小时前
c++ std::tuple用法
开发语言·c++
ysy16480672391 小时前
Javase学习复习D4[流程控制]
学习
Jelena157795857921 小时前
爬虫与翻译API接口的完美结合:开启跨语言数据处理新纪元
开发语言·数据库·爬虫