本课及下一课为选读内容,面向希望深入理解 C++ 模板的读者。部分模板特化在日常开发中使用频率不高,但在特定场景下极为有用。
在课程《模板非类型参数》中,我们学习了如何利用表达式参数来参数化模板类。
再次审视先前示例中的 StaticArray
类:
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]; }
};
该类接收两个模板参数:一个类型参数 T
和一个整型表达式参数 size
。
现在假设我们要编写一个函数来打印整个数组。虽然可以将其作为成员函数实现,但为便于后续示例阅读,我们选择将其写成非成员函数。
模板函数与完全特化
借助模板,我们可能写出如下代码:
template <typename T, int size>
void print(const StaticArray<T, size>& array)
{
for (int count{ 0 }; count < size; ++count)
std::cout << array[count] << ' ';
}
这样就可以:
#include <iostream>
template <typename T, int size>
class StaticArray { /* … */ };
template <typename T, int size>
void print(const StaticArray<T, size>& array) { /* … */ }
int main()
{
StaticArray<int, 4> int4{};
int4[0] = 0; int4[1] = 1; int4[2] = 2; int4[3] = 3;
print(int4);
return 0;
}
输出:
0 1 2 3
尽管可行,但存在设计缺陷。考虑:
#include <algorithm>
#include <iostream>
#include <string_view>
int main()
{
StaticArray<char, 14> char14{};
constexpr std::string_view hello{ "Hello, world!" };
std::copy_n(hello.begin(), hello.size(), char14.getArray());
print(char14);
return 0;
}
程序可正常编译运行,但输出:
H e l l o , w o r l d !
对于非 char
类型,元素之间加空格可避免粘连;而对于 char
类型,连续输出更符合 C 风格字符串的习惯,但当前 print()
并不支持。
于是问题变成:如何修复?
模板特化来救场?
有人首先想到使用模板特化。但完全模板特化要求显式指定所有模板参数。
示例:
// 只为 StaticArray<char, 14> 特化 print()
template <>
void print(const StaticArray<char, 14>& array)
{
for (int count{ 0 }; count < 14; ++count)
std::cout << array[count];
}
确实,print(char14)
将调用此特化并输出:
Hello, world!
然而新问题出现:完全特化必须固定数组长度!若再定义:
StaticArray<char, 12> char12{};
调用 print(char12)
将使用通用模板,因为我们只为 14
提供了特化。为了支持长度 5、22 等,就必须复制特化代码,显然冗余。
完全特化过于受限,我们需要部分模板特化。
部分模板特化
部分模板特化允许我们对类(不能对单独函数!)进行特化,仅显式指定部分模板参数。
针对上述需求,理想的方案是:让重载的 print
仅对 char
类型有效,而长度 size
仍保持模板参数化。部分模板特化正可实现这一点。
示例:
// 针对 StaticArray<char, size> 的重载(非部分特化函数,而是利用类参数的部分特化)
template <int size>
void print(const StaticArray<char, size>& array)
{
for (int count{ 0 }; count < size; ++count)
std::cout << array[count];
}
此处我们显式指定元素类型为 char
,而 size
仍为模板非类型参数,因此可接受任意长度的 char
数组。
完整示例:
#include <algorithm>
#include <iostream>
#include <string_view>
template <typename T, int size>
class StaticArray { /* … */ };
template <typename T, int size>
void print(const StaticArray<T, size>& array) { /* … */ }
template <int size>
void print(const StaticArray<char, size>& array) { /* … */ }
int main()
{
StaticArray<char, 14> char14{};
constexpr std::string_view hello14{ "Hello, world!" };
std::copy_n(hello14.begin(), hello14.size(), char14.getArray());
print(char14);
std::cout << ' ';
StaticArray<char, 12> char12{};
constexpr std::string_view hello12{ "Hello, mom!" };
std::copy_n(hello12.begin(), hello12.size(), char12.getArray());
print(char12);
}
输出:
Hello, world! Hello, mom!
部分模板特化只能用于类,不能用于模板函数(函数必须完全特化)。
本例之所以能写 void print(const StaticArray<char, size>&)
,是因为它是对参数类进行部分特化后,编译器选择的重载,而非函数本身的部分特化。
部分模板特化与成员函数
限制在于:不能对成员函数做部分特化。例如:
template <typename T, int size>
class StaticArray
{
/* … */
void print() const;
};
template <typename T, int size>
void StaticArray<T, size>::print() const { /* … */ }
// 非法:无法部分特化成员函数
template <int size>
void StaticArray<double, size>::print() const { /* … */ }
解决方法是对整个类做部分特化:
template <typename T, int size>
class StaticArray
{
/* … */
void print() const;
};
// 通用实现
template <typename T, int size>
void StaticArray<T, size>::print() const { /* … */ }
// 对 StaticArray<double, size> 的部分特化
template <int size>
class StaticArray<double, size>
{
/* … */
void print() const;
};
template <int size>
void StaticArray<double, size>::print() const
{
/* 科学计数法输出 */
}
完整示例:
#include <iostream>
template <typename T, int size>
class StaticArray
{
/* … */
void print() const;
};
template <typename T, int size>
void StaticArray<T, size>::print() const { /* … */ }
template <int size>
class StaticArray<double, size>
{
double m_array[size]{};
public:
double* getArray() { return m_array; }
const double& operator[](int i) const { return m_array[i]; }
double& operator[](int i) { return m_array[i]; }
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()
{
StaticArray<int, 6> intArray{};
StaticArray<double, 4> doubleArray{};
/* … */
intArray.print();
doubleArray.print();
}
输出示例:
0 1 2 3 4 5
4.000000e+00 4.100000e+00 4.200000e+00 4.300000e+00
但此方法仍需复制大量代码。若能让 StaticArray<double, size>
复用 StaticArray<T, size>
的代码,则更佳——继承即可解决。
利用公共基类减少重复
直接继承:
template <int size>
class StaticArray<double, size> : public StaticArray<T, size> // 错误:T 未定义
无法通过语法实现。即便允许,实例化 StaticArray<double, size>
时 T
会被替换为 double
,导致自身继承自身,逻辑错误。
正确做法是抽取公共基类:
#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 i) const { return m_array[i]; }
T& operator[](int i) { return m_array[i]; }
virtual void print() const
{
for (int i{ 0 }; i < size; ++i)
std::cout << m_array[i] << ' ';
std::cout << '\n';
}
virtual ~StaticArray_Base() = default;
};
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 override
{
for (int i{ 0 }; i < size; ++i)
std::cout << std::scientific << this->m_array[i] << ' ';
std::cout << '\n';
}
};
int main()
{
/* … */
}
结果同上,但代码冗余大幅减少。