C++ 模板参数

在 C++ 中,编写模板时写在 template <...> 尖括号里面的参数,统称为 “模板参数(template parameter)”。前面我们接触的模板大多只声明了单个 typename T,但在实际开发中,模板参数还有更灵活的写法。

比如同一个模板里可能包含多个独立变化的数据类型,有时需要传入一个常量整数来指定数组的固定长度,还有时希望给模板参数设置默认值。围绕这些需求,C++ 提供了 “类型模板参数”、“非类型模板参数” 和 “默认模板参数”。

C++ 类型模板参数

用来代表某一种待定数据类型的模板参数,称为 “类型模板参数”。我们可以拿普通函数的参数来对比:定义模板时写在 template <...> 里的 T 叫做 “模板形参”,使用时填入尖括号里的 int、std::string 等具体类型叫做 “模板实参”。

1. typename 与 class 的区别

在声明类型模板参数时,C++ 支持使用 typename 或 class 关键字,下面 2 种方式是完全等价的:

// 方式 1(推荐)
template <typename T>
void show(T value) {
    // 函数体
}

// 方式 2
template <class T>
void show(T value) {
    // 函数体
}

对于上面这两种写法,小伙伴们只需要清楚以下 2 点:

  • class T 不代表只能传类:很多初学者看到 template <class T>,容易误以为后面只能传入自定义的类。实际上并非如此,int、double、指针等任何数据类型都可以传给 class T,这里的 class 只是早期 C++ 沿用下来的关键字。
  • 推荐统一使用 typename:从字面意思来看,typename 表示 “类型名称”,一眼就能看出这是一个类型参数,也不会和定义类的 class 混淆,因此在实际开发中我们更推荐使用方式 1。

2. 多类型模板参数

前面我们遇到过一个限制:如果模板只声明了一个 typename T,那么调用时直接传入 int 和 double 两个不同类型的实参就会推导失败。

实际上,一个模板并不是只能声明一个类型参数。如果函数模板或类模板中有两个甚至多个彼此独立变化的数据类型,我们可以在尖括号里使用英文逗号(,)隔开,声明多个类型参数。

语法:

// 多类型函数模板
template <typename T, typename U>
返回类型 函数名(T 参数1, U 参数2) {
    // 函数体
}

// 多类型类模板
template <typename K, typename V>
class 类名 {
    // 成员变量与成员函数
};

说明:

在上面的语法中,T 和 U 是两个各自独立的类型参数,在表示键值对时也常写成 K 和 V。使用时,它们既可以接收不同的数据类型,也可以接收相同的数据类型。

示例 1:多类型参数的函数模板

#include <iostream>

template <typename T, typename U>
void printPair(const T& left, const U& right) {
    std::cout << left << " : " << right << '\n';
}

int main() {
    printPair("Jack", 18);
    printPair(1001, 95.5);
    printPair(10, 20);
    return 0;
}

运行结果如下。

Jack : 18
1001 : 95.5
10 : 20

分析:

在这个例子中,printPair() 声明了 T 和 U 两个独立的类型参数。第一次调用传入字符串和 int,第二次调用传入 int 和 double,编译器会根据左右两个实参各自推导出 T 和 U 的类型,互不干扰。

如果像第三次调用那样传入两个 int 整数,T 和 U 也可以同时被推导为 int。多类型模板参数同样常用于类模板中,比如编写一个用来保存 “键(Key)” 和 “值(Value)” 的键值对类模板。下面来看一个例子。

示例 2:多类型参数的类模板

#include <iostream>
#include <string>

template <typename K, typename V>
class KeyValue {
private:
    K key;
    V value;

public:
    KeyValue(const K& k, const V& v) {
        key = k;
        value = v;
    }

    void show() const {
        std::cout << key << " -> " << value << '\n';
    }
};

int main() {
    KeyValue<std::string, int> item1("score", 95);
    item1.show();

    KeyValue<int, std::string> item2(1001, "Lucy");
    item2.show();
    return 0;
}

运行结果如下。

score -> 95
1001 -> Lucy

分析:

在这个例子中,KeyValue<std::string, int> 将第 1 个参数 K 指定为 std::string,将第 2 个参数 V 指定为 int。而创建 item2 时则反过来,将 K 指定为 int,将 V 指定为 std::string。

C++ 非类型模板参数

前面介绍的 T、U、K、V 代表的都是 “某一种数据类型”。除此之外,模板参数还可以是一个在编译阶段就已经确定的常量整数,常用来指定数组的固定长度或容量上限,这种参数称为 “非类型模板参数”。

语法:

template <typename T, int N>
class 类名 {
    // 可将 N 当作常量整数使用
};

说明:

在 template <typename T, int N> 中,typename T 声明的是类型参数,而 int N 声明的则是一个整数类型的非类型模板参数。由于数组长度不会为负数,在标准库中也常将其声明为无符号整型 size_t N。

进入类模板内部后,N 就是一个现成的编译期常量,可以直接拿来定义固定长度的数组 T data[N]。在实际开发中,非类型模板参数最常用的就是 int、size_t 等整数类型,不能把 std::string 字符串这类对象当成非类型模板参数传进去。

示例 3:使用非类型模板参数

#include <iostream>
#include <string>

template <typename T, int N>
class FixedArray {
private:
    T data[N] = {};

public:
    void set(int index, const T& value) {
        if (index >= 0 && index < N) {
            data[index] = value;
        }
    }

    T get(int index) const {
        return data[index];
    }

    int size() const {
        return N;
    }
};

int main() {
    FixedArray<int, 4> intArr;
    intArr.set(0, 10);
    std::cout << intArr.size() << ", " << intArr.get(0) << '\n';

    FixedArray<std::string, 3> strArr;
    strArr.set(0, "C++");
    std::cout << strArr.size() << ", " << strArr.get(0) << '\n';
    return 0;
}

运行结果如下。

4, 10
3, C++

分析:

FixedArray<int, 4> intArr; 表示把类型参数 T 指定为 int,同时把非类型参数 N 指定为整数 4。编译器在实例化时,会把类内部的 T data[N] = {}; 生成为 int data[4] = {};,并将数组元素全部初始化为默认值 0。

在使用非类型模板参数时,小伙伴们需要注意以下 2 条规则:

  • 实参必须是编译期常量:由于模板是在编译阶段生成具体代码的,传给 N 的实参必须在编译阶段就能确定数值,比如整数字面量 4,或者直接用字面量初始化的 const 整型常量,不能传入运行期间才确定值的普通变量。
  • 非类型实参不同也属于不同的类:比如 FixedArray<int, 3> 和 FixedArray<int, 4>,虽然元素类型都是 int,但因为长度实参 3 和 4 不同,编译器会将它们生成为两个完全独立的类,彼此不能直接赋值。

比如在下面的代码中,len 是一个普通整型变量,它的值在运行阶段才确定,作为非类型模板实参传入时就会报错:

int len = 5;
FixedArray<int, len> arr;    // 编译报错

C++ 默认模板参数

和普通函数支持默认参数类似,C++ 的模板参数也支持通过等号(=)指定默认的数据类型或默认整数值,称为 “默认模板参数”。

语法:

template <typename T = 默认类型, int N = 默认整数>
class 类名 {
    // 成员变量与成员函数
};

说明:

在包含多个参数的类模板中,一旦某个模板参数设置了默认值,排在它右边的所有模板参数也都必须设置默认值,不能左边有默认值而右边没有默认值:

// 正确方式
template <typename K, typename V = int>
class PairBox {};

// 错误方式
template <typename K = int, typename V>
class PairBox {};

创建类模板对象时,如果显式填入具体的模板实参,新传入的实参就会覆盖默认值。如果所有模板参数都设有默认值,我们想直接使用默认配置,只需在类名后面写一对空尖括号 类名<> 对象名; 就可以了。

示例 4:指定默认模板参数

#include <iostream>
#include <string>

// 指定默认模板参数
template <typename T = int, int N = 10>
class BufferBox {
private:
    T data[N] = {};

public:
    BufferBox(const T& firstVal) {
        data[0] = firstVal;
    }

    void showInfo() const {
        std::cout << data[0] << ",容量:" << N << '\n';
    }
};

int main() {
    // 全部使用默认参数
    BufferBox<> box1(100);
    box1.showInfo();

    // 只指定第 1 个参数
    BufferBox<std::string> box2("绿叶网");
    box2.showInfo();

    // 覆盖全部默认参数
    BufferBox<double, 50> box3(3.14);
    box3.showInfo();
    return 0;
}

运行结果如下。

100,容量:10
绿叶网,容量:10
3.14,容量:50

分析:

在这个例子中,BufferBox 将类型参数 T 的默认值设为 int,将容量参数 N 的默认值设为 10。创建 box1 时写了空尖括号 BufferBox<>,等价于 BufferBox<int, 10>,两个参数全部采用默认值。

创建 box2 时只传入了 std::string,第 1 个参数 T 被替换为 std::string,省略的第 2 个参数 N 继续使用默认值 10。创建 box3 时传入了 double 和 50,两个默认值则被全部覆盖。

这里小伙伴们要留意:就像调用带默认参数的普通函数仍然要写空小括号 func() 一样,使用类模板的全部默认参数创建对象时,推荐在类名后面保留一对空尖括号写成 BufferBox<> box1(100);,这样一眼就能看出 BufferBox 是一个类模板。

提示 函数模板同样支持设置默认模板参数,比如 template <typename T = int> void show(T a = 0)。与类模板不同的是,调用函数模板时如果全部使用默认参数,直接写 show() 即可,不需要加上空尖括号 “<>”。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号