这是《深入实践C++模板编程》第四章的读书笔记。
模板特例
通过函数模板和类模板,可以为不同类型数据编写统一函数和类。但是现实情况比想象复杂,单一模板很难兼容各种情况。C++还提供了模板特例(template partial specialization):对于某个已有模板,可以为某个或某组模板参数类型另外一种一种变体。
vector
模板特例的一个例子是STL中的vector<T>
。vector<bool>
是vector<T>
的一个特例,专门为高效存储而设计。bool
类型只需占用一个bit,我们自己实现简陋的my_vector
来演示如何实现模板特例。
#include <stdlib.h>
#include <stdexcept>
#include <iostream>
template<typename T>
class my_vector {
T *array;
unsigned size;
unsigned block_size;
public:
explicit my_vector(unsigned bsz): array((T*)malloc(sizeof(T) * bsz)),
size(0), block_size(bsz) {}
~my_vector() {if (array) free(array);}
void push_back(const T& elem) throw (std::runtime_error){
if (size == block_size) {
block_size *= 2;
T* new_array = (T*)realloc(array, block_size * sizeof(T));
if (NULL != new_array) {
array = new_array;
} else {
free(array);
array = NULL;
throw std::runtime_error("Out of memory.");
}
}
array[size++] = elem;
}
T& operator[] (unsigned i) {return array[i];}
unsigned get_mem_size() const {return block_size * sizeof(T);}
};
template<>
class my_vector<bool> {
int *array;
unsigned size;
unsigned block_size;
const static unsigned seg_size = 8 * sizeof(int); // 1 byte = 8 bits
public:
explicit my_vector(unsigned bsz = 1):
array((int*)malloc(sizeof(int) * bsz)), size(0), block_size(bsz) {}
~my_vector() {if (array) free(array);}
void push_back(bool elem) throw (std::runtime_error){
if (size == block_size * seg_size) {
block_size *= 2;
int *new_array = (int*)realloc(array, block_size * sizeof(int));
if (NULL != new_array) {
array = new_array;
} else {
free(array);
array = NULL;
throw std::runtime_error("Out of memory.");
}
}
set(size++, elem);
}
void set(unsigned i, bool elem) {
if (elem) {
array[ i / seg_size] |= (0x1 << (i % seg_size));
} else {
array[i / seg_size] &= ~(0x1 << (i % seg_size));
}
}
bool operator[] (unsigned i) const {
return (array[i / seg_size] & (0x1 << (i % seg_size))) != 0;
}
unsigned get_mem_size() const {
return block_size * sizeof(int);
}
};
int main(int argc, char* argv[]) {
my_vector<char> vi(2);
my_vector<bool> vb(2);
for (unsigned i = 0; i < 20; ++i) {
vi.push_back('a' + i);
vb.push_back((i % 4) == 0);
}
std::cout << "MemSize of my_vector<char> is " << vi.get_mem_size() << std::endl;
std::cout << "MemSize of my_vector<bool> is " << vb.get_mem_size() << std::endl;
for (unsigned i = 0; i < 20; ++i) {
std::cout << " " << vi[i];
}
std::cout << std::endl;
for (unsigned i = 0; i < 20; ++i) {
std::cout << vb[i];
}
std::cout << std::endl;
}
特例是通例的特殊实现,接口应该尽量和通例保持一致。上面保持vector<bool>
的操作符[]
无法返回引用,只是返回了值,这里只是演示,可以通过代理类来实现返回引用。
特例的多种写法
上面特例写法比简单,匹配式很简单。C++标准形式可以实现更复杂的匹配要求,匹配式需要遵循以下几条规则:
- 匹配式写在模板类名之后,用尖括号括起。
- 匹配式使用逗号分隔的项目列表,项目数必须与通例模板参数总数一直。
- 匹配式中各项目类型须与通例对应模板参数类型一直。
- 匹配式项目可以是具体的模板参数值,也可以是特例自己声明的模板参数。
- 当匹配式项目是类型模板参数时,与函数变量类似,模板参数也可以用*、&、const及volatile修饰。
下面是特例匹配例子
// 模板型模板参数的模板
template<typename T, int i> struct S1;
// 模板通例,有三个模板参数:类型参数、非类型参数、模板型参数
template<typename T, int i, template<typename, int> class SP>
struct S;
// 特例1,可匹配S<char, 任意整数, S1>。约束第一个参数为char类型,其后的i和SP是特例本身的模板参数。
template<int i, template<typename, int> class SP>
struct<char, i, SP>;
//特例2,可匹配S<任意有const修饰的类型, 任意整数, S1>。约束第一个模板参数须用const修饰。
template<typename T, int i, template<typename, int> class SP>
struct S<const T, i, SP>;
// 特例3,完全特例,只能匹配S<char, 10, S1>。匹配式中没有用到任何模板参数,所有类型都是确定的,所以模板参数列表为空。
template<>
struct S<char, 10, S1>;
// 特例4,以模板实例作为类型参数值。匹配S<S1<任意类型, 10>, 10, S1>。匹配式第一项是一个模板实例S1<T, 10>,这是一个类型,基于模板S1自动生成。第三项S1是一个合法的模板类型参数值。
template<typename T>
struct S<S1<T, 10>, 10, S1>;
// 特例5,错误!匹配式项目数与通例参数个数不一致
template<typename T, int i, template<typename, int> class SP, typename TT>
struct S<T, i, SP, TT>;
// 特例6,错误!匹配项目类型与通例参数类型不同。通例中第三个参数是模板,不是类型。
template<typename T>
struct S<char, 10, T>;
// 特例7,错误!模板类型参数SP与通例中SP类型不一致。
template<typename T, int i, template<typename> class SP>
struct S<const T, i, SP>;
特例匹配规则
同一个模板可以有多个特例,那么如果某套模板参数值能匹配多个特例时,该怎么办?C++的原则是与最“特殊”的特例匹配。比较两个特例A、B,如果所有能匹配A的模板都能匹配B,反之不成立的话,可以确定A比B更加“特殊”。用集合的概念讲,当所有匹配A的参数数值组成的集合,是所有匹配B参数集合的真子集时,可以说A比B更加“特殊”。
编译器根据此原则,从一系列匹配特例中优选最“特殊”的特例。如果编译器无法确定谁更“特殊”,则报错。
#include <iostream>
#include <string>
template<typename T0, typename T1, typename T2>
struct S {
std::string id() {return "General";}
};
// 特例1,约束第三个参数必须为char
template<typename T0, typename T1>
struct S<T0, T1, char> {
std::string id() {return "Specialization #1";}
};
// 特例2,第二和第三个参数必须为char
template<typename T0>
struct S<T0, char, char> {
std::string id() {return "Specialization #2";}
};
// 特例3,第一个参数为int,且第二和第三个参数相同
template<typename T>
struct S<int, T, T> {
std::string id() {return "Specialization #3";}
};
int main(int argc, char* argv[]) {
std::cout << S<float, float, float>().id() << std::endl; // 通例
std::cout << S<int, int, int>().id() << std::endl; // 特例3
std::cout << S<int, int, char>().id() << std::endl; // 特例1
std::cout << S<char, char, char>().id() << std::endl; // 特例2
// std::cout << S<int, char, char>().id() << std::endl; // 有歧义,同时匹配到特例2和特例3
return 0;
}
函数模板的特例与重载
函数模板特例写法和类模板特例写法相同。函数还可以重载,这个函数特例非常类似。
函数重载允许函数名相同,参数不同;调用时根据参数类型选用合适的函数。函数模板也可以重载,即定义函数名相同,但是模板参数不同。函数重载和类模板重载:
- 相同点:都是从多个候选中选择与实参类型最匹配的一个。
- 不同点:函数重载是匹配函数参数;函数模板重载特例是匹配模板参数。
因为函数模板还有参数推导机制,当没有显式给定模板实参值时,编译器会尝试有函数调用的实参类型推导出模板参数值。所以函数模板参数值也和函数调用的实参相关。
因为特例和重载两种机制并存,使得函数模板变得复杂;为了简化问题,C++标准允许为函数模板声明完全特例,禁止部分特例。大多数要用到部分特例的情况可以使用函数模板重载来实现。
以一组格式化输出为例,这组函数名字为print
,接受一个参数并将其打印出来。对于不同类型参数,打印时加上标记:
- 输出单个字符时,字符前后加上单引号,入’a’。
- 输出C风格字符串(即字符指针
char *
),字符串前后加上双引号,例如"a string"。 - 输出
std::string
,前后加上三个单引号,例如’’‘a string’’’。 - 输出指针类型,所指内容前用星号(*)引导。
#include <string>
#include <iostream>
template<typename T>
void print(T v) {
std::cout << v << std::endl;
}
// 模板特例
template<>
void print<char>(char v) {
std::cout << '\'' << v << '\'' << std::endl;
}
// 模板特例
template<>
void print(const char* v) {
std::cout <<'"' << v << '"' << std::endl;
}
// 函数重载
inline
void print(const std::string& v) {
std::cout << "\'\'\'" << v << "\'\'\'" << std::endl;
}
// inline
void print(bool v) {
std::cout << (v ? "true": "flase") << std::endl;
}
// 函数模板重载
template<typename T>
void print(T* v) {
std::cout<< '*';
print(*v);
}
对于单个字符、字符指针、std::string
以及bool
类型,参数类型已经确定,可以使用完全特例或者函数重载。对于任意类型指针,因为具体类型不确定,所以必须写成重载函数模板。
函数模板,模板参数常常和参数类型相关,所以完全模板特例函数,可以省略模板参数,例如void print<char>(char v)
可以省略为void print(char v)
。
函数模板特例,省略了模板参数后,形式上和函数重载只是相差template<>
。这个关键字是分辨函数模板和函数重载的重要依据。
分辨重载
对于一个函数调用,可以存在多个函数模板实例及普通函数作为候选;和类模板特例类似,这时也存在不同候选之间的优先级问题。由于函数模板特例和函数重载同时存在,以及类模板参数可以由调用实参类型推导,使得问题更加复杂。
#include <iostream>
// #1
template<typename T>
void func(T v) {std::cout << "#1:" << v << std::endl;}
// #2
template<>
void func(float v) {std::cout << "#2:" << v << std::endl;}
// #3
void func(float v) {std::cout << "#3:" << v << std::endl;}
// #4
void func2(float v) {std::cout << "#4:" << v << std::endl;}
int main(int argc, char* argv[]) {
func2(1); // 输出 #4: 1
func(1); // 输出 #1: 1
func(1.); // 输出 #1: 1
func(1.f); // 输出 #3: 1
func<>(1.f); // 输出 #2: 1
return 0;
}
模板特例2和函数重载3的参数都是浮点类型参数,但是这并不违反C++的唯一性原则。模板不是普通函数,即使模板特例没有模板参数,但仍然是一个模板。编译器只有在明确需要生成模板实例时才会依据模板内容生成具体函数。所以该重载模板并不与该函数重载冲突。
编译器编译时已知函数名字、参数个数及类型。如果有同名重载函数模板时,会根据参数推导模板参数类型,如果推导成功,则生成响应模板实例作为候选;如果有同名函数,也列为候选。但是当明确要求使用模板实例时(函数后有尖括号),则不将普通重载函数列为候选。
候选集合中的函数模板实例(非模板,是模板实例)和普通函数都可以看作有效的函数定义。可以按照对普通重载函数调用**分辨重载(overload resolution)**规则确定最佳选择。
- 准则1 两个候选函数中,如果有一方其形参列表各类型与调用实参列表各类型更匹配,则淘汰另一方。
匹配程度由高到低依次为等价类型、标准类型转换以及自定义类型转换。等价类型例如char[]
和char *
,以及由实参类型char *
转换成形参类型const char *
。标准类型转换,例如int
转换为long
。自定义类型转换,例如int
转换为用户自定义类型A
,且A
有构造函数A(int)
。
// #1
void func(char* a, long b, long c, long d);
// #2
void func(char a[], long b, int c, long d);
// #3
void func(char a[], long b, long c, int d);
//void call(char a[], int b, int c, int d) {
func(a, b, c, d);
}
如果没有#3,将会调用#2;如果有#3,编译器无法区分#2和#3,将会报错。
- 准则2 两函数如果形参列表类型同等匹配实参列表类型时,若一方为函数模板实例,另一方为非模板函数,优选选取非模板函数。
- 准则3 两函数如果其形参列表类型同等匹配实参列表类型时,若两者都是函数模板实例,取更为“特殊”的一方。
编译期的条件判断逻辑
函数模板的所有参数都与函数参数类型相关时,特例完全可以通过重载函数或函数模板实现。
以一个例子说明模板独特用处:不使用循环和条件判断,打印1~100。
这个问题可以通过递归,归队的终止就是模板特例
#include <iostream>
template<int i>
void print() {
print<i - 1>();
std::cout << i << std::endl;
}
// 特例,终止递归
template<>
void print<1>() {
std::cout << 1 << std::endl;
}
int main(int argc, char* argv[]) {
print<100>();
return 0;
}