26-5 部分模板专门化
本课及下一课为选读内容,供希望深入了解 C++ 模板的读者参考。部分模板特化并不常用(但在某些特定情况下可能很有用)。
在第 26.2 课——模板非类型参数中,你学习了如何使用表达式参数来参数化模板类。
让我们再来看一下之前示例中用到的静态数组类:
template <typename T, int size> // size is the expression parameter
class StaticArray
{
private:
// The expression parameter controls the size of the array
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] << ' ';
}
这将使我们能够做到以下几点:
#include <iostream>
template <typename T, int size> // size is a template non-type parameter
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()
{
// declare an int array
StaticArray<int, 4> int4{};
int4[0] = 0;
int4[1] = 1;
int4[2] = 2;
int4[3] = 3;
// Print the array
print(int4);
return 0;
}
并得到以下结果:

虽然这种方法可行,但它存在设计缺陷。请考虑以下几点:
#include <algorithm>
#include <iostream>
#include <string_view>
int main()
{
// Declare a char array
StaticArray<char, 14> char14{};
// Copy some data into it
constexpr std::string_view hello{ "Hello, world!" };
std::copy_n(hello.begin(), hello.size(), char14.getArray());
// Print the array
print(char14);
return 0;
}
(我们在第17.10 课——C 风格字符串(如果您需要复习一下)中讲解了 std::strcpy。 )
该程序将编译、执行并生成以下值(或类似值):

对于非字符类型,在每个数组元素之间添加空格是合理的,这样它们就不会连在一起。但是,对于字符类型,更合理的做法是将所有内容作为 C 风格的字符串一起打印,而我们的 print() 函数并没有这样做。
那么我们该如何解决这个问题呢?
模板化能特例派上用场吗?
人们首先可能会想到使用模板特化。但完全模板特化的缺点在于,所有模板参数都必须显式定义。
考虑:
#include <algorithm>
#include <iostream>
#include <string_view>
template <typename T, int size> // size is the expression parameter
class StaticArray
{
private:
// The expression parameter controls the size of the array
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] << ' ';
}
// Override print() for fully specialized StaticArray<char, 14>
template <>
void print(const StaticArray<char, 14>& array)
{
for (int count{ 0 }; count < 14; ++count)
std::cout << array[count];
}
int main()
{
// Declare a char array
StaticArray<char, 14> char14{};
// Copy some data into it
constexpr std::string_view hello{ "Hello, world!" };
std::copy_n(hello.begin(), hello.size(), char14.getArray());
// Print the array
print(char14);
return 0;
}
如您所见,我们现在提供了一个功能更强大的打印函数,用于完全专用的操作StaticArray<char, 14>。实际上,它会打印:

虽然这解决了确保 print() 函数可以接受数组参数的问题StaticArray<char, 14>,但它又引出了另一个问题:使用完全模板特化意味着我们必须显式定义该函数接受的数组长度!请看以下示例:
int main()
{
// Declare a char array
StaticArray<char, 12> char12{};
// Copy some data into it
constexpr std::string_view hello{ "Hello, mom!" };
std::copy_n(hello.begin(), hello.size(), char12.getArray());
// Print the array
print(char12);
return 0;
}
StaticArray<T, size>使用 print()调用将调用接受char12的版本,因为char12是StaticArray<T, size>char12类型,而我们重载的 print() 只有在传递StaticArray<char, 14> 时才会被调用。
虽然我们可以复制一个处理数组StaticArray<char, 12>大小为 5 或 22 的 print() 函数,但如果我们想用数组大小为 5 或 22 的数组调用 print() 函数该怎么办?我们必须为每种不同的数组大小都复制一个函数。这很冗余。
显然,完全模板特化在这里过于局限。我们需要的是部分模板特化。
部分模板特化
部分模板特化允许我们对类(而非单个函数!)进行特化,其中部分模板参数已被显式定义。对于我们上面提到的挑战,理想的解决方案是让重载的 print 函数能够处理 char 类型的 StaticArray,但将长度表达式参数保留为模板,使其可以根据需要变化。部分模板特化恰好可以实现这一点!
以下是一个重载打印函数的示例,该函数接受一个部分特化的静态数组:
// overload of print() function for partially specialized StaticArray<char, size>
template <int size> // size is still a template non-type parameter
void print(const StaticArray<char, size>& array) // we're explicitly defining type char here
{
for (int count{ 0 }; count < size; ++count)
std::cout << array[count];
}
如您所见,我们已明确声明此函数仅适用于 char 类型的 StaticArray,但 size 仍然是一个模板表达式参数,因此它适用于任何大小的 char 数组。就是这样!
以下是使用该技术的完整程序:
#include <algorithm>
#include <iostream>
#include <string_view>
template <typename T, int size> // size is the expression parameter
class StaticArray
{
private:
// The expression parameter controls the size of the array
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] << ' ';
}
// overload of print() function for partially specialized 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()
{
// Declare an char array of size 14
StaticArray<char, 14> char14{};
// Copy some data into it
constexpr std::string_view hello14{ "Hello, world!" };
std::copy_n(hello14.begin(), hello14.size(), char14.getArray());
// Print the array
print(char14);
std::cout << ' ';
// Now declare an char array of size 12
StaticArray<char, 12> char12{};
// Copy some data into it
constexpr std::string_view hello12{ "Hello, mom!" };
std::copy_n(hello12.begin(), hello12.size(), char12.getArray());
// Print the array
print(char12);
return 0;
}
打印出来的内容:

正如我们所料。
部分模板特化只能用于类,不能用于模板函数(函数必须完全特化)。我们的void print(StaticArray<char, size> &array)示例之所以有效,是因为 print 函数没有进行部分特化(它只是一个重载的模板函数,恰好有一个部分特化的类参数)。
成员函数的部分模板特化
函数的部分特化限制在处理成员函数时会带来一些挑战。例如,如果我们像这样定义 StaticArray 会怎样?
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() 进行部分特化,使其以不同的方式工作时,会发生什么呢?你可以尝试这样做:
// Doesn't work, can't partially specialize functions
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';
}

很遗憾,这样做行不通,因为我们试图对一个函数进行部分特化,这是不允许的。
那么我们该如何解决这个问题呢?一个显而易见的办法是对整个类进行部分特例化:
#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';
}
// Partially specialized class
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;
};
// Member function of partially specialized class
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()
{
// declare an integer array with room for 6 integers
StaticArray<int, 6> intArray{};
// Fill it up in order, then print it
for (int count{ 0 }; count < 6; ++count)
intArray[count] = count;
intArray.print();
// declare a double buffer with room for 4 doubles
StaticArray<double, 4> doubleArray{};
for (int count{ 0 }; count < 4; ++count)
doubleArray[count] = (4.0 + 0.1 * count);
doubleArray.print();
return 0;
}
打印出来的内容:

之所以有效,是因为StaticArray<double, size>::print()它不再是一个部分特化的函数——它是部分特化类的一个非特化成员StaticArray<double, size>。
然而,这并不是一个好的解决方案,因为我们需要将大量代码从 复制StaticArray<T, size>到StaticArray<double, size>。
如果能找到某种方法来重用代码就好StaticArray<T, size>了StaticArray<double, size>。听起来像是继承的用武之地!
你可以尝试这样编写代码:
template <int size> // size is the expression parameter
class StaticArray<double, size>: public StaticArray<T, size>
但这行不通,因为我们T没有定义就使用了它。没有语法允许我们以这种方式继承。
顺便提一下: 即使我们能够将其定义T为类型模板参数,当StaticArray<double, size>实例化时,编译器也需要将 StaticArray<T, size>中的
T替换为实际类型。那么它会使用什么实际类型呢?唯一合理的类型是T=double,但这又会导致StaticArray<double, size>继承自身!
幸运的是,我们可以通过使用通用基类来解决这个问题:
#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';
}
// Don't forget a virtual destructor if you're going to use virtual function resolution
};
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] << ' ';
// note: The this-> prefix in the above line is needed.
// See https://stackoverflow.com/a/6592617 or https://isocpp.org/wiki/faq/templates#nondependent-name-lookup-members for more info on why.
std::cout << '\n';
}
};
int main()
{
// declare an integer array with room for 6 integers
StaticArray<int, 6> intArray{};
// Fill it up in order, then print it
for (int count{ 0 }; count < 6; ++count)
intArray[count] = count;
intArray.print();
// declare a double buffer with room for 4 doubles
StaticArray<double, 4> doubleArray{};
for (int count{ 0 }; count < 4; ++count)
doubleArray[count] = (4.0 + 0.1 * count);
doubleArray.print();
return 0;
}

这段代码打印的内容与上面的相同,但重复的代码明显更少。

浙公网安备 33010602011771号