章节目录

类模板特化

本节阅读量:

在上一课中,我们看到了如何特化函数,以便为特定数据类型提供不同的功能。事实证明,不仅函数可以特化,类也可以特化!

考虑这样一种情况,您需要一个存储8个对象的类。下面是一个简化的类模板:

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

template <typename T>
class Storage8
{
private:
    T m_array[8];

public:
    void set(int index, const T& value)
    {
        m_array[index] = value;
    }

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

int main()
{
    // 定义int的 Storage8
    Storage8<int> intStorage;

    for (int count{ 0 }; count < 8; ++count)
        intStorage.set(count, count);

    for (int count{ 0 }; count < 8; ++count)
        std::cout << intStorage.get(count) << '\n';

    // 定义bool的 Storage8
    Storage8<bool> boolStorage;
    for (int count{ 0 }; count < 8; ++count)
        boolStorage.set(count, count & 3);

	std::cout << std::boolalpha;

    for (int count{ 0 }; count < 8; ++count)
    {
        std::cout << boolStorage.get(count) << '\n';
    }

    return 0;
}

此示例会打印:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
0
1
2
3
4
5
6
7
false
true
true
true
false
true
true
true

这个类完全是为了演示,Storage8<bool>的实现效率很低。由于所有变量都必须有地址,而CPU不能寻址小于一个字节的内容,因此所有变量的大小都至少是一个字节。因此,bool类型的变量最终会使用整个字节,即使从技术上讲,它只需要一个bit位来存储true或false值!也就是说,一个布尔值包含1位有用信息和7位浪费空间。我们的Storage8<bool>类包含8个bool,相当于1个字节的有用信息和7个字节的浪费空间。

事实证明,使用一些基本的位运算,可以将所有8个布尔值压缩到单个字节中,从而完全消除浪费的空间。然而,为了做到这一点,当该类与bool类型一起使用时,我们需要修改它的实现,将8个bool组成的数组替换为一个单字节变量。虽然我们可以创建一个全新的类来完成这件事,但这有一个缺点:我们必须给它起一个不同的名称。然后,程序员必须记住,Storage8<T>用于非布尔类型,而Storage8Bool(或我们命名的新类)用于bool。这是我们希望避免的不必要复杂性。幸运的是,C++提供了一种更好的方法:类模板特化。


类模板特化

类模板特化允许我们为特定模板类编写特化版本。在这种情况下,我们将使用类模板特化来编写Storage8<bool>的定制版本,该版本会优先于通用的Storage8<T>类。类模板特化会被视为完全独立的类,即使它们的实例化方式与模板化类相同。这意味着我们可以改变特化类的任何内容,包括实现方式,甚至公开的函数,就像它本身就是一个独立的类一样。

和所有模板一样,编译器必须能够看到特化的完整定义,才能使用它。定义类模板特化前,需要先定义非特化的类。

下面是专用Storage8<bool>类的示例:

 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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <cstdint>

// 首先定义非特化的类模板
template <typename T>
class Storage8
{
private:
    T m_array[8];

public:
    void set(int index, const T& value)
    {
        m_array[index] = value;
    }

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

// 现在定义特化的类模板
template <> // 下面是无模板参数的类模板
class Storage8<bool> // 针对 bool 特化 Storage8
{
// 下面是标准的类实现细节

private:
    std::uint8_t m_data{};

public:
    // 不用完全操心这里的实现细节
    void set(int index, bool value)
    {
        // 找到要操作的bit
        // 然后我们会给对应的bit赋值
        auto mask{ 1 << index };

        if (value)  // 如果需要set 对应的bit 为 1
            m_data |= mask;   // 使用 bit - or 来进行操作 
        else  // 如果需要设置对应的 bit 为 0
            m_data &= ~mask;  // 使用 bit - and 来进行操作
	}

    bool get(int index)
    {
        // 找到要读取的bit
        auto mask{ 1 << index };
        // bit - and 提取对应位置的值
        // 结果会隐式的转换为 bool
        return (m_data & mask);
    }
};

// 一些使用样例
int main()
{
    // 定义int的 Storage8  (实例化 Storage8<T>, T = int)
    Storage8<int> intStorage;

    for (int count{ 0 }; count < 8; ++count)
    {
        intStorage.set(count, count);
	}

    for (int count{ 0 }; count < 8; ++count)
    {
        std::cout << intStorage.get(count) << '\n';
    }

    // 定义bool的 Storage8  (实例化 Storage8<bool> 特化版本)
    Storage8<bool> boolStorage;

    for (int count{ 0 }; count < 8; ++count)
    {
        boolStorage.set(count, count & 3);
    }

	std::cout << std::boolalpha;

    for (int count{ 0 }; count < 8; ++count)
    {
        std::cout << boolStorage.get(count) << '\n';
    }

    return 0;
}

首先,请注意,我们的特化类模板以template<>开始。template关键字告诉编译器后面是模板,空的尖括号表示没有任何模板参数。在这种情况下,不需要模板参数,因为我们已经将唯一的模板参数(T)替换为特定类型(bool)。

接下来,我们将<bool>添加到类名中,表示我们特化了类Storage8的bool版本。

其他所有更改都只是类的实现细节。为了使用该类,您不需要理解位逻辑是如何工作的。

请注意,该特化类使用std::uint8_t(1字节无符号int)。

现在,当我们实例化Storage8<T>类型的对象,且T不是bool时,会得到从通用Storage8<T>类模板生成的版本。当我们实例化Storage8<bool>类型的对象时,会得到刚刚创建的专用版本。请注意,我们保持了两个类的公开接口一致——虽然C++允许我们根据需要添加、删除或更改Storage8<bool>的函数,但保持一致的接口意味着程序员可以用完全相同的方式使用这两个类。

正如您所料,这会打印与上一个使用Storage8<bool>非特化版本的示例相同的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
0
1
2
3
4
5
6
7
false
true
true
true
false
true
true
true

特化成员函数

在上一课中,我们介绍了以下示例:

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

template <typename T>
class Storage
{
private:
    T m_value {};
public:
    Storage(T value)
      : m_value { value }
    {
    }

    void print()
    {
        std::cout << m_value << '\n';
    }
};

int main()
{
    // 定义一些 storage
    Storage i { 5 };
    Storage d { 6.7 };

    // 进行打印
    i.print();
    d.print();
}

我们的目标是特化print()函数,让它以科学记数法打印双精度值。使用类模板特化,我们可以为Storage定义一个特化类:

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

template <typename T>
class Storage
{
private:
    T m_value {};
public:
    Storage(T value)
      : m_value { value }
    {
    }

    void print()
    {
        std::cout << m_value << '\n';
    }
};

// 显式特化 Storage<double> 类
// 注意下面有大量的重复
template <>
class Storage<double>
{
private:
    double m_value {};
public:
    Storage(double value)
      : m_value { value }
    {
    }

    void print();
};

// 在外面定义函数,因为这样会让类声明简短
// 这是一个普通(非特化)成员函数定义(它是Storage<double>类的成员函数)
void Storage<double>::print()
{
    std::cout << std::scientific << m_value << '\n';
}

int main()
{
    // 定义一些 storage
    Storage i { 5 };
    Storage d { 6.7 }; // 使用特化版本 Storage<double>

    // 进行打印
    i.print(); // 调用 Storage<int>::print (实例化 Storage<T>)
    d.print(); // 调用 Storage<double>::print (实例化显式特化的 Storage<double>)
}

然而,请注意这里有大量冗余。为了更改一个成员函数,我们复制了整个类定义!

幸运的是,我们可以做得更好。C++并不要求我们为了显式特化Storage<double>::print()而显式特化整个Storage<double>类。相反,我们可以让编译器从Storage<T>中隐式实例化Storage<double>,并且只为Storage<double>::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
#include <iostream>

template <typename T>
class Storage
{
private:
    T m_value {};
public:
    Storage(T value)
      : m_value { value }
    {
    }

    void print()
    {
        std::cout << m_value << '\n';
    }
};

// 这是一个成员函数显式特化
// 显式成员函数特化不是隐式 inline, 所以如果要放在头文件要手动标记为inline
template<>
void Storage<double>::print()
{
    std::cout << std::scientific << m_value << '\n';
}

int main()
{
    // 定义一些 storage
    Storage i { 5 };
    Storage d { 6.7 }; // 会导致 Storage<double> 实例化

    // 进行打印
    i.print(); // 调用 Storage<int>::print (实例化 Storage<T>)
    d.print(); // 调用 Storage<double>::print (实例化显式特化的 Storage<double>::print())
}

就这样!

如前一课所述,显式函数特化不是隐式内联的,因此如果在头文件中定义Storage<double>::print(),应将其标记为inline。


定义类模板特化的位置

为了使用特化,编译器必须能够看到非特化类和特化类的完整定义。如果编译器只能看到非特化类的定义,它就会使用该定义,而不是特化版本。

由于这个原因,特化类和特化函数通常定义在头文件中非特化类的下方,因此包含单个头文件时,会同时包含非特化类和所有特化类。这可以确保只要能看到非特化类,就始终能看到特化类。

如果只在单个翻译单元中需要特化,则可以在该翻译单元的源文件中定义它。由于其他翻译单元看不到特化的定义,它们会继续使用非特化版本。

注意不要将特化放在单独的头文件中,因为这会要求我们在每个需要特化的翻译单元中额外包含该头文件。设计一种会因头文件是否存在而悄悄改变行为的代码,是一个坏主意。例如,如果您打算使用特化,但忘记包含特化头文件,最终可能会使用非特化版本。如果您打算使用非特化版本,而某个已包含的头文件又间接包含了特化头文件,最终也可能意外使用特化版本。


26.2 函数模板特化

上一节

26.4 部分模板特化

下一节