一、类模板初探

类模板的实现

下面是一个简单的Stack模板类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
template <typename T>
class Stack {
public:
Stack() = default;
~Stack() = default;

void push(const T& value) {
stack.push_back(value);
}

void pop() {
assert(!stack.empty());
stack.pop_back();
}

const T& top() const {
assert(!stack.empty());
return stack.back();
}

bool empty() const {
return stack.empty();
}

size_t size() const {
return stack.size();
}
private:
std::vector<T> stack;
};

为了方便,我们使用C++标准库中的vector<>来实现这个类。

类模板的声明

我们实现的stack的类型是Stack<T>,不同于函数模板,每次使用Stack创建对象时,都要显式说明T的类型,如Stack<int> stack;(若该类在构造时可以通过构造函数推断出所有类型参数,则可以不显式指定)。

如果我们在类模板内定义和使用该类,可以选择不显式指定模板实参的类名,这时默认是以外部Stack模板的类型相同。

如:

1
2
3
4
5
6
7
template <typename T>
class Stack {
...
Stack (Stack const&);
Stack& operator= (Stack const&);
...
}

二、部分使用类模板

下面我们来明确一个事实,如果我们定义了一个模板类,并为它提供了大量的、丰富的成员函数共类操作,当我们去实例化这个模板类时,它的所有成员函数会不会一起被实例化?答案是否定的,编译器相当聪明,并不会无脑去实例化所有的实现,它只实例化实际用到的成员函数。

来看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename T>
class Example
{
public:
Example() = default;
Example(const T &value) : value(value) {}
bool operator<(const Example &example) const
{
return this->value < example.value;
}

private:
T value;
};
int main()
{
std::ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);

Example<int> example1 = 1, example2 = 2;
cout << (example1 < example2) << '\n';
Example<Stack<int>> example3, example4;
return 0;
}

注意到,我们实际并没有为Stack类重载<号,那么Example模板中有对<号的使用,猜想它应该会报错:因为如果我们创建了Example<Stack<int>>类型的实例,但我们却没有为Stack<T>类型重载<号——但是程序正常运行了。

唯一的解释方法就是,我们虽然实例化了Example<Stack<int>>类型的类,但是却没有对它使用<号,所以它内部并没有实例化<的重载函数,因此没有报错。

假如我们加上一句

1
cout << (example3 < example4) << '\n';

程序在编译前,编辑器或IDE没有报错,因为模板是编译器才会实例化相应类型的代码的;编译时,出现了报错error: no match for 'operator<' (operand types are 'const Stack<int>' and 'const Stack<int>') return this->value < example.value;,说明我们没有重载Stack<int><运算符。

概念

我们如何知道模板实例化需要哪些操作?

术语概念经常用于表示模板中可重复使用的约束集,C++标准库依赖随机访问迭代器默认可构造等概念。

目前,C++标准并没有成体系地详细指明这个概念,未对其进行标准化,这可能会在编译期出现可怕的错误。

从C++11开始,我们可以通过static_assert和一些预定义类型特征检查基本约束:

1
static_assert(std::is_default_constructible<T>::value, "Class requires sefault-constructible elements");

我们可能仍无法在编译前提前知道错误,但是报错信息可能会明确很多——如果不加这个信息,那么报错信息可能会从实例化初始触发点到检测到错误的实际模板定义处。

友元介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
void printOn(std::ostream &out) const
{
for (const T &value : stack)
{
out << value << ' ';
}
out << '\n';
}
friend std::ostream &operator<<(std::ostream &out, const Stack &stack)
{
stack.printOn(out);
return out;
}

注意我们并没有对operator<<使用模板,而只是在传参中用到了Stack,所以它并不能称作函数模板,而是一个随模板实例化的函数实体

然而,当我们只想声明一个友元函数时,却非常复杂:

如果我们想简单地:

在类中声明:

1
friend std::ostream &operator<<(std::ostream &out, const Stack<T> &stack);

在类外定义:

1
2
3
4
5
6
template <typename T>
std::ostream &operator<<(std::ostream &out, const Stack<T> &stack)
{
stack.printOn(out);
return out;
}

然后使用

1
cout << example3 << '\n';

编译发现,它非常恐怖地报了几百行的错误,开始为:warning: friend declaration 'std::ostream& operator<<(std::ostream&, const Stack<T>&)' declares a non-template function [-Wnon-template-friend] friend std::ostream &operator<<(std::ostream &out, const Stack<T> &stack);

声明友元函数是一个非模板函数。

这也印证了我们之前的那句话,我们并没有对operator<<使用函数模板,而是将它作为一个普通函数,当我们实例化一个Stack对象并调用operator<<时,只是将它作为普通函数完成了声明,没有调用类外部的函数模板,也就是没有实例化相应的函数定义。

下面有两种解决方案:

类内使用函数模板

类内:

1
2
template <typename U>
friend std::ostream &operator<<(std::ostream &out, const Stack<U> &stack);

类外:

1
2
3
4
5
6
template <typename U>
std::ostream &operator<<(std::ostream &out, const Stack<U> &stack)
{
stack.printOn(out);
return out;
}

注意这里使用了模板参数U而非T,来重新声明该成员函数是一个模板。

将友元函数作为模板,让类内函数作为友元函数的实例

注意我们的声明顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
template <typename T>
class Stack;
template <typename T>
std::ostream &operator<<(std::ostream &out, const Stack<T> &stack);

template <typename T>
class Stack
{
public:
Stack() = default;
~Stack() = default;

void push(const T &value)
{
stack.push_back(value);
}

void pop()
{
assert(!stack.empty());
stack.pop_back();
}

const T &top() const
{
assert(!stack.empty());
return stack.back();
}

bool empty() const
{
return stack.empty();
}

size_t size() const
{
return stack.size();
}

void printOn(std::ostream &out) const
{
for (const T &value : stack)
{
out << value << ' ';
}
out << '\n';
}
friend std::ostream &operator<< <T>(std::ostream &out, const Stack<T> &stack); // 在这里实例化typename T类型的operator<<

private:
std::vector<T> stack;
};
template <typename T>
std::ostream &operator<<(std::ostream &out, const Stack<T> &stack)
{
stack.printOn(out);
return out;
}

三、偏特化

单个参数类型的偏特化

如果我们有两个模板类:

1
2
3
4
5
template <typename T>
class Stack;

template <typename T>
class Stack<T*>;

下面的T*版本作为上面普通模板版本的偏特化而存在,如果我们传入一个int类型的参数,那么毫无疑问它将调用上方的构造函数,因为int无法与<T*>相匹配,但是如果我们传入一个int*类型,<T><T*>都符合要求,这时就会因为偏特化优先级高于普通类模板,而调用下方的构造函数。

多参数类型的偏特化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
template <typename T1, typename T2>
class A
{
private:
T1 a;
T2 b;
public:
A(T1 a, T2 b) : a(a), b(b) {}
void print()
{
cout << a << ' ' << b << '\n';
}
};
template <typename T>
class A<T, int>
{
public:
void print()
{
cout << "T int\n";
}
void print_partial(T a, int b)
{
cout << a << ' ' << b << '\n';
}
};
int main()
{
std::ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
A<int, int> a(1, 2);
a.print();
a.print_full(3, 4);
return 0;
}

我们可以指定其中一个参数的类型(可以直接指定,或是像上面那种将类型加以区分),来调用不同的模板偏特化版本。