浅窥C++模板编程

Posted Coder学习路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅窥C++模板编程相关的知识,希望对你有一定的参考价值。

近期学习和编写代码时接触了一些关于C++模板编程的知识,深觉模板元编程是一个非常有趣的世界。因此写下本文,分享一些C++模板的一些知识点和应用,希望能对你有些帮助。

1. STL真香

提到模板,就不得不提C++语言的标准模板库( Standard Template Library, STL )。STL包含4个组件,分别为算法、容器、函数、迭代器,对于大多数C++程序员来说,容器应该是日常编码中最常使用的。
在利用容器时,我们就已经接触了模板。如 vector<int> ,这表示用 int 类型实例化模板 vector 。常用容器还有 deque set map 等,它们都属于模板类。此外,还有函数模板,如STL中提供的各种算法,它们都属于函数模板。
利用STL可以极大地提高编程效率,省去了自己实现常用数据结构和算法的时间。试想使用C语言编程,则当需要 stack queue 等最基础的数据结构或者 sort merge 等算法时,都需要自行实现。而使用C++语言,这些都只是一行代码的问题。因此,当习惯了使用STL之后再转向C语言编程时,一定免不了感叹一句:STL真香,也让人感叹模板编程的魅力。

2. 如何在编译期计算阶乘

模板编程和我们平时写程序时很大一点不同就是:模板编程在编译期间会做很多事情,例如最基本的模板实例化,甚至进行一些基础的计算。这里举一个例子:如何利用模板,实现在编译期计算阶乘?
先看代码:
template <unsigned int n>
struct fact
{

    enum
    {
        value = n * fact<n - 1>::value
    };
};

template <>
struct fact<0>
{

    enum
    {
        value = 1
    };
};

int main()
{
    cout << fact<5>().value << endl;    // 输出 120 (5!)  
    return 0;
}

对模板编程不太熟悉的读者,可能会感到惊讶:我只知道类型名可以作为模板形参,这里整型 unsigned int 竟然也可以做函数形参!是的,整型的确可以作为模板形参,而这正是在编译期进行计算的关键。
上面代码中,首先声明了一个名为 fact 的模板结构体,结构体内部又声明了一个匿名枚举类型,枚举类型中只包含一个变量: value  ,而  value = n * fact<n - 1>::value 。有点递归的感觉,也有点计算阶乘的感觉。既然像递归,那么递归结束条件是什么呢?
答案就在后面:进行模板特例化,在特例化结构体 fact<0> 中, value 的值为1,代表  0! = 1 。至此就完成了利用模板计算阶乘。需要注意的是,在上述代码中,阶乘的计算是在编译期完成的,在运行期, main 函数只需要输出 120 即可,并不会在运行期进行乘法运算。
这也揭示了模板编程的一个应用:将计算从运行期移动到编译期,这样就可以提高程序执行速度。 不过相应的代价是程序的编译时间会变长。

3. 编译期确定数组长度

为了说明模板编程的神奇,此处在举一个例子:在编译器确定数组长度。代码非常简洁,但却不是很好懂:
#include <iostream>
using namespace std;

template <typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept   
{
    return N;
}

int main()
{
    int a[] = {123456};
    const char name[] = "ad sdfs-s sd,sas";
    cout << "arraySize = " << arraySize(a) << endl;
    cout << "arraySize = " << arraySize(name) << endl;   

    char copy[arraySize(name)] = {'a''b''c'};

    return 0;
}

最关键的就是那一个函数模板:模板参数为类型 T 和类型 std::size_t ,其中  std::size_t 为C++提供的一种类型别名,在我的机器中(64位),其就是64位的无符号整型: typedef unsigned __int64 size_t; 。函数形参为 T (&)[N] ,即 T 类型长度为 N 的数组的引用。返回类型为  std::size_t ,同时声明了这个函数为  constexpr constexpr 是 C++ 11 标准的一个关键字,代表一个常量表达式。函数体只有一条语句: return N;
上述代码中,利用模板和 constexpr 共同完成了在编译器计算数组的长度,并以常量的形式返回。
当然,上述代码只是为了说明模板编程的能力而举的例子,在实际编程中,我们计算数组长度时更多地是利用 sizeof 运算符,如 sizeof(a)/sizeof(int) ,这样即可计算名称为 a 的 int 类型数组的长度。

4. 打印任意类型的序列容器

这里再举一个例子,说明模板编程的作用:打印任意类型的容器。
代码主要包括两个部分,一为打印C++内置类型( int char double 等)的一系列重载函数;二为一个函数模板。代码如下:
// 重载基本类型,结合下面 template ,则可以打印任何元素为基本类型的容器
void print(char c)
{
    cout << c;
}

void print(int i)
{
    cout << i;
}

template <typename T>
void print(const T &container)
{
    for (auto &elem : container)
        print(elem);
    cout << endl;
}

作为示意,打印 C++ 内置类型的函数只实现了 int 类型和 char 类型。最关键的是下面的函数模板。在这个函数中,模板类型为 T,在函数体内部,利用 范围for循环 ,递归调用 print 函数,即可实现打印任意类型容器。
这个例子代表了一种典型的模板编程技术:首先实现一个通用的模板函数(上面的 print 函数模板),用来处理绝大多数情况;然后对于一些边界情况或者特殊情况(如上面的打印内置类型的函数),特例化模板,进行单独实现。

5. 将任意内置类型转为二进制字符串

作为本文的最后一个例子,来介绍一个实用的功能。平时编写代码时,我们经过遇到这样的需求:将一个数的二进制形式打印出来。这一需求就可以利用模板技术方便地实现,代码如下:
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

template <typename T>
string showBit(T n)
{
    string res;
    int bit_width = sizeof(n) * 8// 二进制位数
    for (int i = 0; i < bit_width; ++i, n >>= 1)
        res.push_back((1 & n) ? '1' : '0');
    reverse(res.begin(), res.end()); // 翻转
    return res;
}

string showBit(float f)
{
    int *pi = reinterpret_cast<int *>(&f); // 强制类型转换
    return showBit<int>(*pi);
}

string showBit(double d)
{
    long long *pl = reinterpret_cast<long long *>(&d);
    return showBit<long long>(*pl);
}

string showBit(long double d)
{
    long long *pl = reinterpret_cast<long long *>(&d);
    return showBit<long long>(*pl);
}

int main()
{
    cout << showBit(INT_MAX) << endl;
    cout << showBit(INT_MIN) << endl;
    cout << showBit(UINT32_MAX) << endl;
    cout << showBit(-1) << endl;
    cout << showBit('a') << endl;
    cout << showBit(1.2f) << endl;
    cout << showBit(1.2) << endl;
    cout << showBit(static_cast<long double>(0.0)) << endl;
    cout << showBit(0.0) << endl;
    return 0;
}

相信经过前文的介绍,上面的代码不难理解。它还是利用了一个模板函数+若干个特例化模板。其中,模板函数只能处理整型,因为其中用到了移位运算符  >> ,而只有整型允许移位运算。特例化函数主要是为了处理浮点型数据,在函数内利用了一个小小的编程技巧:利用指针,将浮点型对应的二进制串不加改变的变为整型,然后再调用模板函数即可将浮点类型转换为二进制串。
上面代码的运行结果截图为:
从运行结果来看,成功完成了转二进制的功能,与预期相同。


以上就是本文全部内容。尽管只是非常浅显的介绍,但从中也可窥探出C++模板编程的强大力量。实际上,C++模板元编程已经发展成为一个独特的分支,目前也不乏对模板技术和模板元编程进行详细介绍的书籍,推荐对模板编程感兴趣的读者阅读。