部分模板特化

本课及下一课为选读内容,面向希望深入理解 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()
{
    /* … */
}

结果同上,但代码冗余大幅减少。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"