章节目录

部分模板特化

本节阅读量:

对于希望更深入了解C++模板的读者来说,这一课和下一课属于可选阅读。部分模板特化并不常用(但在特定情况下可能很有用)。

让我们再看一下前面示例中使用过的StaticArray类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
template <typename T, int size> // size 是一个非类型参数
class StaticArray
{
private:
    // size 控制数组的大小
    T m_array[size]{};
 
public:
    T* getArray() { return m_array; }
	
    const T& operator[](int index) const { return m_array[index]; }
    T& operator[](int index) { return m_array[index]; }
};

此模板类接受两个模板参数:一个类型参数和一个非类型参数。

现在,假设我们想编写一个函数来打印整个数组。尽管可以将其实现为成员函数,但这里会将它实现为非成员函数,因为这样后续示例更容易理解。

使用模板,我们可以这样编写:

1
2
3
4
5
6
template <typename T, int size>
void print(const StaticArray<T, size>& array)
{
    for (int count{ 0 }; count < size; ++count)
        std::cout << array[count] << ' ';
}

这样就可以执行以下操作:

 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
#include <iostream>

template <typename T, int size> // size 是一个非类型参数
class StaticArray
{
private:
	T m_array[size]{};

public:
	T* getArray() { return m_array; }

	const T& operator[](int index) const { return m_array[index]; }
	T& operator[](int index) { return m_array[index]; }
};

template <typename T, int size>
void print(const StaticArray<T, size>& array)
{
	for (int count{ 0 }; count < size; ++count)
		std::cout << array[count] << ' ';
}

int main()
{
	// 定义 int 数组
	StaticArray<int, 4> int4{};
	int4[0] = 0;
	int4[1] = 1;
	int4[2] = 2;
	int4[3] = 3;

	// 打印
	print(int4);

	return 0;
}

并得到以下结果:

1
0 1 2 3

尽管这样可行,但它存在一个设计缺陷。请考虑以下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <algorithm>
#include <iostream>
#include <string_view>

int main()
{
    // 定义 char 数组
    StaticArray<char, 14> char14{};

    // 拷贝一些数据到数组里
    constexpr std::string_view hello{ "Hello, world!" };
    std::copy_n(hello.begin(), hello.size(), char14.getArray());

    // 打印
    print(char14);

    return 0;
}

该程序可以通过编译并运行,产生以下输出(或类似输出):

1
H e l l o ,   w o r l d !

对于非char类型,在每个数组元素之间放一个空格是有意义的,这样它们就不会挤在一起显示。然而,对于char类型,将所有内容连在一起打印为C样式字符串更合理,而我们的print()函数并没有这样做。

那么我们如何解决这个问题呢?


模板部分特化?

我们可能首先会考虑使用模板特化。完全模板特化的问题在于,必须显式定义所有模板参数。

考虑:

 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
#include <algorithm>
#include <iostream>
#include <string_view>

template <typename T, int size> // size 是一个非类型参数
class StaticArray
{
private:
	// size 控制数组的大小
	T m_array[size]{};

public:
	T* getArray() { return m_array; }

	const T& operator[](int index) const { return m_array[index]; }
	T& operator[](int index) { return m_array[index]; }
};

template <typename T, int size>
void print(const StaticArray<T, size>& array)
{
	for (int count{ 0 }; count < size; ++count)
		std::cout << array[count] << ' ';
}

// 重写 print(),针对完全特化 StaticArray<char, 14>
template <>
void print(const StaticArray<char, 14>& array)
{
	for (int count{ 0 }; count < 14; ++count)
		std::cout << array[count];
}

int main()
{
    // 定义 char 数组
    StaticArray<char, 14> char14{};

    // 拷贝一些数据到数组里
    constexpr std::string_view hello{ "Hello, world!" };
    std::copy_n(hello.begin(), hello.size(), char14.getArray());

    // 打印
    print(char14);

    return 0;
}

如您所见,我们现在为完全特化的StaticArray<char, 14>提供了重载打印函数。它会打印:

1
Hello, world!

尽管这解决了StaticArray<char, 14>调用print()的问题,但也带来了另一个问题:使用完全模板特化意味着我们必须显式定义该函数可接受的数组长度!考虑以下示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int main()
{
    //  定义 char 数组
    StaticArray<char, 12> char12{};

    // 拷贝一些数据到数组里
    constexpr std::string_view hello{ "Hello, mom!" };
    std::copy_n(hello.begin(), hello.size(), char12.getArray());

    // 打印
    print(char12);

    return 0;
}

使用char12调用print()时,会调用采用StaticArray<T, size>参数的print()版本,因为char12的类型是StaticArray<char, 12>。

尽管我们可以再制作一个处理StaticArray<char, 12>的print()副本,但当我们想处理大小为5或22的数组时又该怎么办?我们必须为每个不同的数组大小复制函数。这显然是多余的。

显然,完全模板特化在这里限制太强。我们真正需要的解决方案是部分模板特化。


部分模板特化

部分模板特化允许我们特化类(但不是单个函数!),其中一部分模板参数(但不是全部)会被显式定义。对于上面的挑战,理想的解决方案是让重载的print函数适用于char类型的StaticArray,同时保留长度表达式参数的模板化,让它可以根据需要变化。部分模板特化允许我们做到这一点!

下面是一个重载print函数的示例,该函数采用部分特化的StaticArray:

1
2
3
4
5
6
7
// 重载 print() 函数,部分特化 StaticArray<char, size>
template <int size> // size 仍然是非类型模板参数
void print(const StaticArray<char, size>& array) // 这里显示指定第一个参数是char
{
	for (int count{ 0 }; count < size; ++count)
		std::cout << array[count];
}

正如您在这里看到的,我们已经明确声明该函数仅适用于char类型的StaticArray,但size仍然是模板化的表达式参数,因此它适用于任意大小的char数组!

下面是一个使用该函数的完整程序:

 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
59
#include <algorithm>
#include <iostream>
#include <string_view>

template <typename T, int size> // size 是一个非类型参数
class StaticArray
{
private:
	// size 控制数组的大小
	T m_array[size]{};

public:
	T* getArray() { return m_array; }

	const T& operator[](int index) const { return m_array[index]; }
	T& operator[](int index) { return m_array[index]; }
};

template <typename T, int size>
void print(const StaticArray<T, size>& array)
{
	for (int count{ 0 }; count < size; ++count)
		std::cout << array[count] << ' ';
}

// 重载 print() 函数,部分特化 StaticArray<char, size>
template <int size>
void print(const StaticArray<char, size>& array)
{
	for (int count{ 0 }; count < size; ++count)
		std::cout << array[count];
}

int main()
{
	// 定义长度为 14 的 char 数组
	StaticArray<char, 14> char14{};

	// 拷贝一些数据到数组里
	constexpr std::string_view hello14{ "Hello, world!" };
	std::copy_n(hello14.begin(), hello14.size(), char14.getArray());

	// 打印
	print(char14);

	std::cout << ' ';

	// 定义长度为 12 的 char 数组
	StaticArray<char, 12> char12{};

	// 拷贝一些数据到数组里
	constexpr std::string_view hello12{ "Hello, mom!" };
	std::copy_n(hello12.begin(), hello12.size(), char12.getArray());

	// 打印
	print(char12);

	return 0;
}

这会打印:

1
Hello, world! Hello, mom!

结果正如我们所料。

部分模板特化只能用于类,不能用于模板函数(函数必须完全特化)。我们的void print(StaticArray<char, size>& array)示例可以工作,是因为print函数并不是部分特化的(它只是一个重载的模板函数,碰巧具有一个部分特化的类参数)。


成员函数的部分模板特化

在处理成员函数时,函数不能部分特化这一限制可能会带来一些挑战。例如,如果我们这样定义StaticArray,会怎么样?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <typename T, int size>
class StaticArray
{
private:
    T m_array[size]{};
 
public:
    T* getArray() { return m_array; }
	
    const T& operator[](int index) const { return m_array[index]; }
    T& operator[](int index) { return m_array[index]; }

    void print() const;
};

template <typename T, int size> 
void StaticArray<T, size>::print() const
{
    for (int i{ 0 }; i < size; ++i)
        std::cout << m_array[i] << ' ';
    std::cout << '\n';
}

print()现在是类StaticArray<T, int>的成员函数。那么,当我们希望部分特化print(),让它以不同方式工作时,会发生什么?您可以尝试以下操作:

1
2
3
4
5
6
7
8
// 无法编译, 不能部分特化函数
template <int size>
void StaticArray<double, size>::print() const
{
	for (int i{ 0 }; i < size; ++i)
		std::cout << std::scientific << m_array[i] << ' ';
	std::cout << '\n';
}

不幸的是,这行不通,因为我们正在尝试部分特化函数,而这是不允许的。

那么该如何解决这个问题呢?一种明显的方法是部分特化整个类:

 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
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <iostream>

template <typename T, int size>
class StaticArray
{
private:
	T m_array[size]{};

public:
	T* getArray() { return m_array; }

	const T& operator[](int index) const { return m_array[index]; }
	T& operator[](int index) { return m_array[index]; }

	void print() const;
};

template <typename T, int size> 
void StaticArray<T, size>::print() const
{
	for (int i{ 0 }; i < size; ++i)
		std::cout << m_array[i] << ' ';
	std::cout << '\n';
}

// 部分特化类
template <int size>
class StaticArray<double, size>
{
private:
	double m_array[size]{};

public:
	double* getArray() { return m_array; }

	const double& operator[](int index) const { return m_array[index]; }
	double& operator[](int index) { return m_array[index]; }

	void print() const;
};

// 部分特化类的成员函数
template <int size>
void StaticArray<double, size>::print() const
{
	for (int i{ 0 }; i < size; ++i)
		std::cout << std::scientific << m_array[i] << ' ';
	std::cout << '\n';
}

int main()
{
	// 定义有 6 个int的数组
	StaticArray<int, 6> intArray{};

	// Fill it up in order, then print it
	for (int count{ 0 }; count < 6; ++count)
		intArray[count] = count;

	intArray.print();

	// 定义有 4 个double的数组
	StaticArray<double, 4> doubleArray{};

	for (int count{ 0 }; count < 4; ++count)
		doubleArray[count] = (4.0 + 0.1 * count);

	doubleArray.print();

	return 0;
}

这会打印:

1
2
0 1 2 3 4 5
4.000000e+00 4.100000e+00 4.200000e+00 4.300000e+00

这是因为StaticArray<double, size>::print()不再是部分特化函数——它是部分特化类StaticArray<double, size>的非特化成员函数。

然而,这不是一个好方案,因为我们必须将大量代码从StaticArray<T, size>复制到StaticArray<double, size>。

如果能用某种方法重用StaticArray<T, size>中的代码就好了。听起来这正适合用继承来解决!

您可能会先尝试这样编写代码:

1
2
template <int size> // size 是非类型模板参数
class StaticArray<double, size>: public StaticArray<T, size>

但这行不通,因为我们使用T时并没有定义它。没有任何语法允许我们以这种方式继承。

幸运的是,我们可以通过public继承基类来绕开这个问题:

 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
59
60
61
62
63
64
65
#include <iostream>

template <typename T, int size>
class StaticArray_Base
{
protected:
	T m_array[size]{};

public:
	T* getArray() { return m_array; }

	const T& operator[](int index) const { return m_array[index]; }
	T& operator[](int index) { return m_array[index]; }

	void print() const
	{
		for (int i{ 0 }; i < size; ++i)
			std::cout << m_array[i] << ' ';
		std::cout << '\n';
	}

	// 如果要使用虚函数,不要忘记析构函数声明为虚函数
};

template <typename T, int size>
class StaticArray: public StaticArray_Base<T, size>
{
};

template <int size>
class StaticArray<double, size>: public StaticArray_Base<double, size>
{
public:

	void print() const
	{
		for (int i{ 0 }; i < size; ++i)
			std::cout << std::scientific << this->m_array[i] << ' ';
// 注: this-> 前缀这里是必须的
// 细节请看 https://stackoverflow.com/a/6592617 或 https://isocpp.org/wiki/faq/templates#nondependent-name-lookup-members
		std::cout << '\n';
	}
};

int main()
{
	// 定义6个int的数组
	StaticArray<int, 6> intArray{};

	// 按顺序填充并打印
	for (int count{ 0 }; count < 6; ++count)
		intArray[count] = count;

	intArray.print();

	// 定义有6个double的数组
	StaticArray<double, 4> doubleArray{};

	for (int count{ 0 }; count < 4; ++count)
		doubleArray[count] = (4.0 + 0.1 * count);

	doubleArray.print();

	return 0;
}

它会打印与上面相同的结果,但重复代码少得多。


26.3 类模板特化

上一节

26.5 指针的部分模板特化

下一节