模板参数和实参
模板类型接收参数:
- “类型类型”的类型参数
- 内置类型值参数,如
int
和函数指针 - “模板类型”的模板参数
一个模板可以接受固定数量或可变数量的参数。
注意,一种很常见的模板类型实参命名方法是采用首字母大写的短名字,如 T
、C
、Cont
和 Ptr
。这是可以接受的,因为这种名字按惯例常用于相对较小的作用域。但是,当使用 ALL_CAPS
这种名字时,就很可能同宏名冲突。因此不要使用太长的名字,以免与宏名冲突。
类型作为实参
通过使用 typename
或 class
关键字,可以将一个模板实参定义为类型参数(type parameter)。语法上,typename
和 class
的作用相同,具体示例如下:
template<typename T>
void f(T);
类型实参是无约束的。换句话说,模板参数可以是任何类型,并没有限制它必须是某种特定类型或者类层次的一部分。在使用时,模板类和函数会根据传入的实参类型进行推断,生成特定的实现。
模板的类型参数可以是用户定义类型或者内置类型,C++ 不会因此产生额外的空间开销。
一个类型必须在作用域内且可访问,才能作为模板实参。
值作为实参
非类型或模板的模板参数称为值实参,传递给它的实参成为值实参。
template<typename T, int max>
class Buffer {
T v[max];
public:
Buffer() { }
// ...
};
传递给模板的值参数可以是以下几种类型:
- 整型常量表达式。
- 指向外部链接的对象或函数的指针或引用。
- 指向非重载成员指针。
-
nullptr
指针。
C++ 不允许将浮点数和字符串字面量作为模板实参。例如:
X<int, "BMW323Ci"> x1; // 错误:字符串字面值作为模板实参
char lx2[] = "BMW323Ci";
X<int, lx2> x2; // 正确:lx2 为外部链接
在上面的代码中,直接使用字符串字面量 "BMW323Ci"
作为模板实参是非法的,而使用具有外部链接的字符数组则是合法的。这种限制是为了简化编译器的实现,并避免在模板参数中引入浮点和字符串等复杂类型。
模板中的值参数在模板内部是一个常量,因此试图修改该值是非法的。例如:
template<typename T, int max>
class Buffer {
T v[max];
public:
Buffer(int i) { max = i; } // 错误:max 为模板值参数
// ...
};
在模板参数列表中,类类型模板参数可以用作后续的参数类型。这一特性与默认模板实参结合使用特别有用。例如:
template<typename T, T default_value = T{} >
class Vec {
// ...
};
Vec<int, 42> c1; // default_value = 42
Vec<int> c2; // default_value = 0
操作作为实参
C++ 标准库中的 map
容器需要为键值(Key)提供一个比较准则。通常来说,容器无法对其元素的键值定义固定的比较规则,因为不同类型的键值可能需要不同的比较方式。因此,map
默认会使用 <
运算符进行元素比较,但并非所有的 Key
类型都支持 <
运算符。
为了灵活地支持不同类型的键值,map
允许用户自定义比较准则。可以通过以下两种方式实现:
- 使用一个模板值参数(例如,传递一个指向比较函数的指针)。
- 通过
map
模板的一个类型实参,指定一个比较对象的类型。
使用一个模板值参数(比较函数指针)
一种方式是传入特定类型的比较对象。示例代码如下:
template<typename Key, typename V, bool (*cmp)(const Key&, const Key&)>
class map {
public:
map(); // ...
};
如果我们定义了一个不区分大小写的字符串比较函数 insensitive
,可以如下使用:
bool insensitive(const string& x, const string& y) {
// 忽略大小写比较
return tolower(x) < tolower(y);
}
map<string, int, insensitive> m; // 使用 insensitive 进行比较
在上面的示例中,map
通过传入的 insensitive
函数实现了不区分大小写的字符串比较。但这种方法的灵活性有限,因为 map
的设计者必须确定使用一个函数指针来比较 Key
。
使用类型参数作为比较规则
更常用的做法是将比较准则作为模板类型参数传递,C++ 标准库中的 map
也是采用这种方式。可以使用类似以下的代码:
template<typename Key, typename V, typename Compare = std::less<Key>>
class map {
public:
map(); // 使用默认比较
map(Compare c); // 用特定比较规则初始化
Compare cmp {}; // 默认比较
// ...
};
在上面代码中,std::less<Key>
是默认的比较规则。如果用户希望使用不同的比较规则,可以通过自定义的 Compare
类型替代默认的 std::less
。例如:
map<string, int> m1; // 使用默认 less<string> 比较
map<string, int, std::greater<string>> m2; // 使用 greater<string> 比较
这种方式相比直接传入函数指针更加灵活,因为 Compare
可以是任何类型的函数对象,而不仅仅是一个函数指针。
函数对象的使用
函数对象(functor)可以携带状态,这使得它比简单的函数指针更为灵活。例如:
struct Complex_compare {
string locale;
Complex_compare(string loc) : locale(loc) {}
bool operator()(const string& a, const string& b) const {
// 使用 locale 进行比较
}
};
map<string, int, Complex_compare> m3({"French"});
在这里,Complex_compare
是一个包含状态(locale
)的函数对象,允许比较函数根据 locale
的不同实现不同的比较规则。
lambda 表达式的使用
我们还可以使用 lambda
表达式来定义比较函数对象,例如:
using Cmp = bool(*)(const string&, const string&);
map<string, int, Cmp> m4(insensitive);
map<string, int, Cmp> m4([](const string& a, const string& b) { return a > b; });
auto cmp = [](const string& x, const string& y) { return x < y; };
map<string, int, decltype(cmp)> c4(cmp);
模板作为实参
在某些情况下,将一个模板(而不是类或值)作为模板实参传递给另一个模板是有用的。以下代码展示了如何将模板作为实参传递:
template<typename T, template<typename> class C>
class Xrefd {
C<T> mems; // 使用模板 C 生成 T 类型的对象
C<T*> refs; // 使用模板 C 生成 T* 类型的对象
// ...
};
在上面的代码中,Xrefd
模板的第二个模板参数 C
是一个模板类,因此我们可以传递不同的模板(例如 vector
)来生成 Xrefd
的不同实例。以下是具体的示例:
template<typename T>
class My_container {
// 自定义容器的实现
};
Xrefd<Record, My_container> x2; // 在 My_container 中存储 Record 的交叉引用
为了将一个模板用作模板实参,我们必须明确指出该模板的参数。例如,在定义 Xrefd
时,我们指明 C
是一个模板类,它接受单个类型参数。如果不这样做,我们就无法使用 C
的特化版本。
只有类模板才能用作模板实参。
默认模板实参
类似于默认参数实参,我们只能对尾部模板参数指定和提供默认实参。
#include <string>
using std::string;
template<typename Target = string, typename Source = string>
Target to(const Source& arg)
{
stringstream interpreter;
Target result;
if (!(interpreter << arg) || !(interpreter >> result) || !(interpreter >> std::ws).eof()) {
throw runtime_error("to<>() failed");
}
return result;
}
auto x1 = to<string, double>(1.2); // 显式指定 Target 为 string, Source 为 double
auto x2 = to<string>(1.2); // 省略 Source,推断为 double
auto x3 = to<>(1.2); // Target 推断为 string, Source 推断为 double
auto x4 = to(1.2); // 使用默认的模板参数,省略 <>
特例化
默认情况下,模板具有单一定义,适用于所有可能的模板实参组合。然而,某些设计场景下,设计者可能希望根据模板实参的不同选择不同的实现。例如,针对指针类型的实参使用特定的实现,或限制模板实参为特定基类的派生类指针。这类需求可以通过提供多个可选的模板定义来实现,编译器根据用户提供的模板实参选择合适的定义。这种技术称为用户自定义特例化(user-defined specialization),允许用户为模板提供特定的实现。
特例化技术主要用于有效控制代码膨胀,并且在不影响代码泛化性的前提下提供更优的性能。(为特定的类型,提供特定的优化)
特例化版本可提供与通用模板不同的实现方式:
#include <iostream>
template<typename T, int N>
class Matrix; // T 的 N 维矩阵
template<typename T>
class Matrix<T, 0> { // N==0 的特例化版本
public:
T val;
// ...
};
template<typename T>
class Matrix<T, 1> { // N==1 的特例化版本
public:
T* elem;
int sz; // 元素个数
// ...
};
template<typename T>
class Matrix<T, 2> { // N==2 的特例化版本
public:
T* elem;
int dim1; // 行数
int dim2; // 列数
// ...
};
int main() {
// 定义一个0维的Matrix
Matrix<int, 0> m0;
m0.val = 42; // 直接给定值
std::cout << "0维矩阵的值: " << m0.val << std::endl;
// 定义一个1维的Matrix (向量)
Matrix<int, 1> m1;
m1.sz = 5; // 假设有5个元素
m1.elem = new int[m1.sz] {1, 2, 3, 4, 5}; // 动态分配并初始化
std::cout << "1维矩阵的值: ";
for (int i = 0; i < m1.sz; ++i) {
std::cout << m1.elem[i] << " ";
}
std::cout << std::endl;
delete[] m1.elem; // 释放内存
// 定义一个2维的Matrix (矩阵)
Matrix<int, 2> m2;
m2.dim1 = 2; // 行数
m2.dim2 = 3; // 列数
m2.elem = new int[m2.dim1 * m2.dim2] {1, 2, 3, 4, 5, 6}; // 动态分配并初始化
std::cout << "2维矩阵的值:" << std::endl;
for (int i = 0; i < m2.dim1; ++i) {
for (int j = 0; j < m2.dim2; ++j) {
std::cout << m2.elem[i * m2.dim2 + j] << " ";
}
std::cout << std::endl;
}
delete[] m2.elem; // 释放内存
return 0;
}
完整特例化
#include <iostream>
#include <vector>
#include <typeinfo>
template <typename T>
class Test {
public:
T value;
// 初始化函数
Test(T value) : value(value) {
// 打印类型
std::cout << "Type: " << typeid(T).name() << std::endl;
}
};
// 针对 void* 的特例化
template <>
class Test<void*> {
public:
void* value;
// 初始化函数
Test(void* value) : value(value) {
// 打印类型
std::cout << "Type: void*" << std::endl;
}
};
// 测试代码
int main() {
Test<int*> test1(nullptr);
Test<float*> test2(nullptr);
Test<void*> test4(nullptr);
return 0;
}
运行输出:
Type: int * __ptr64
Type: float * __ptr64
Type: void*
前缀 template<>
表示这是一个不必指明模板参数的特例化版本。特例化版本所使用的模板实参由模板名后面括号 <>
中的内容指定。即,<void*>
指出这个定义将用于所有 T
为 void*
的 Vector
。
完整特例化(complete specialization):在使用此特例化版本时不用再指定或推断任何模板参数。
部分特例化
#include <iostream>
#include <vector>
#include <typeinfo>
template <typename T>
class Test {
public:
T value;
// 初始化函数
Test(T value) : value(value) {
// 打印类型
std::cout << "Type: " << typeid(T).name() << std::endl;
}
};
// 部分特例化
template<typename T>
class Test<T*> {
public:
T* value;
Test(T* value) : value(value) {
// 打印类型
std::cout << "Type: T*" << std::endl;
}
};
// 测试代码
int main() {
Test<int*> test1(nullptr);
Test<float*> test2(nullptr);
Test<void*> test4(nullptr);
return 0;
}
运行输出:
Type: T*
Type: T*
Type: T*
部分特例化:对一个模板的部分参数或特性进行特化,而不要求完全指定模板参数。
如何理解,对于特例化版本的定义来说:
template<typename T>
相当于声明一下后面要使用的类型参数,方便后面定义特例化时使用。
主模板
主模板是最通用的模板定义,用于提供默认的接口。所有特例化版本都基于主模板定义,并且在模板解析时,编译器会优先考虑主模板,只有在特定的模板实参与特例化版本匹配时,才会选择特例化版本。
为了定义特例化版本,通常需要先声明(或直接定义)主模板,例如:
template<typename T>
class List; // 声明主模板,不是定义
template<typename T>
class List<T*>; // 针对指针类型的特例化
如果主模板在程序中从未实例化,则无须定义。基于此,我们可以定义只接受几种特定实参组合的模板。
函数模板特例化
在 C++ 中,函数模板特例化可以对模板函数进行定制化,使其在处理特定参数类型时表现出不同的行为。函数模板的特例化在使用场景上不如类模板特例化常见,但在处理某些特定类型时非常有效。
template<typename T>
bool less(T a, T b) {
return a < b;
}
template<>
bool less<const char*>(const char* a, const char* b) {
return strcmp(a, b) < 0; // 针对 const char* 类型的特例化
}
函数模板特例化在实际应用中的场景有限,但一个常见的用例是在无实参的函数中进行选择。例如,定义一个 max_value
模板函数,用于获取不同数据类型的最大值:
template<typename T>
T max_value(); // 无定义
template<>
constexpr int max_value<int>() { return INT_MAX; }
template<>
constexpr char max_value<char>() { return CHAR_MAX; }
在上面的代码中,max_value
模板函数针对 int
和 char
类型进行了特例化,以返回对应类型的最大值。这些特例化版本是 constexpr
的,确保可以在编译期计算出结果。
template<typename Iter>
Iter my_algo(Iter p)
{
auto x = max_value<Value_type<Iter>>(); // 根据迭代器的值类型选择特例化版本
// ...
}
本文介绍了C++模板参数的三种类型:类型参数、值参数和操作参数。
1. **类型参数**:使用`typename`或`class`关键字定义,可以是任何类型,无约束。类型参数在模板内部无额外空间开销,且必须是作用域内且可访问的。
2. **值参数**:非类型或模板的模板参数,传递给它的实参成为值参数。值参数可以是整型常量表达式、指向外部链接的对象或函数的指针或引用、指向非重载成员指针、`nullptr`指针。C++不允许浮点数和字符串字面量作为模板值参数。值参数在模板内部是常量,不可修改。
3. **操作参数**:用于指定比较准则等操作。C++标准库中的`map`容器允许用户自定义比较准则。可以通过模板值参数传递比较函数指针,或通过类型参数传递比较对象类型。使用类型参数更灵活,因为可以传递任意类型的函数对象,而不仅仅是函数指针。函数对象可以携带状态,比简单函数指针更灵活。
总结来说,C++模板参数提供了高度的灵活性和通用性,允许开发者根据需要定制类型、常量值和操作。