模板超编程

✍ dations ◷ 2024-09-20 16:28:02 #计算机编程,C++

模板超编程(英语:Template metaprogramming,缩写:TMP)是一种超编程技术,编译器使用模板产生暂时性的源码,然后再和剩下的源码混合并编译。这些模板的输出包括编译时期常量、数据结构以及完整的函数。如此利用模板可以被想成编译期的运行。这种技术被许多语言使用,最为知名的当属C++,其他还有Curl、D、Eiffel,以及语言扩展,如Template Haskell(英语:Template Haskell)。

使用模板作为元编程的技术需要两阶段的操作。首先,模板必须被定义;第二,定义的模板必须被实体化才行。模板的定义描述了生成源码的一般形式,而使实体化则导致了某些源码的组合根据该模板而生成。

模板元编程是一般性地图灵完全(),这意味着任何可被电算软件表示的运算都可以透过模板超编程以某种形式去运算。

模板与宏()是不同的。所谓宏只不过是以文字的操作及替换来生成代码,虽然同为编译期的语言特色,但宏系统通常其编译期处理流的能力()有限,且对其所依附之语言的语义及类型系统缺乏认知(一个例外是LISP的宏)。

模板元编程没有可变的变量——也就是说,变量一旦初始化后就不能够改动。因此他可以被视为函数式编程()的一种形式。

模板超编程的语法通常与一般的程序语法迥异,他有其实际的用途。一些使用模板超编程的共同理由是为了实现泛型编程()或是展现自动编译期最优化,像是只要在编译期做某些事一次就好,而无需每次程序运行时去做。

以下将展示究竟何谓"编译期程序设计"。阶乘是一个不错的例子,在此之前我们先来回顾一下一般C++中端乘函数的递归写法:

int factorial(int n) {    if (n == 0)       return 1;    return n * factorial(n - 1);}void foo(){    int x = factorial(4); // == (4 * 3 * 2 * 1 * 1) == 24    int y = factorial(0); // == 0! == 1}

以上的代码会在程序运行时决定4和0的阶乘。

现在让我们看看使用了模板超编程的写法,模板特化提供了"递归"的结束条件。这些阶乘可以在编译期完成计算。以下源码:

template <int N>struct Factorial {    enum { value = N * Factorial<N - 1>::value };};template <>struct Factorial<0> {    enum { value = 1 };};// Factorial<4>::value == 24// Factorial<0>::value == 1void foo(){    int x = Factorial<4>::value; // == 24    int y = Factorial<0>::value; // == 1}

代码如上在编译时期计算4和0的阶乘值,使用该结果值仿佛他们是预定的常量一般。

虽然从程序功能的观点来看,这两个版本很类似,但前者是在运行期计算阶乘,而后者却是在编译期完成计算。然而,为了能够以此方式使用模板,编译器必须在编译期知道模板的参数值,也就是Factorial<X>::value只有当X在编译期已知的情况下才能使用。换言之,X必须是常量()或是常量表示式(),像是使用sizeof运算符。

以上阶乘的示例便是编译期代码最优化的一例,该程序中使用到的阶乘在编译期时便被预先计算并作为数值常量植入运行码当中,节省了运行期的经常开销(计算时间)以及存储器足迹()。

编译期循环展开()是一个更显著的例子,模板超编程可以被用来产生n维()的向量类别(当然n必须在编译期已知)。与传统n维向量比较,他的好处是循环可以被展开,这可以使性能大幅度提升。考虑以下例子,n维向量的加法可以被写成:

template <int dimension>Vector<dimension>& Vector<dimension>::operator+=(const Vector<dimension>& rhs) {    for (int i = 0; i < dimension; ++i)        value += rhs.value;    return *this;}

当编译器实体化以上的模板函数,可能会生成如下的代码:

template <>Vector<2>& Vector<2>::operator+=(const Vector<2>& rhs) {    value += rhs.value;    value += rhs.value;    return *this;}

因为模板参数dimension在编译期是常量,所以编译器应能展开for循环。

多态是一项共通的标准编程工具,派生类的对象可以被当作基类的对象之实体使用,但能够调用派生对象的函数,或称方法(),例如以下的代码:

class Base{    public:    virtual void method() { std::cout << "Base"; }};class Derived : public Base{    public:    virtual void method() { std::cout << "Derived"; }};int main(){    Base *pBase = new Derived;    pBase->method(); //outputs "Derived"    delete pBase;    return 0;}

唤起的 virtual 函数是属于位于继承最下位之类别的。这种动态多态()行为是借由拥有虚函数的类别所产生的虚函数表()来实行的。虚函数表会在运行期被查找,以决定该唤起哪个函数。因此动态多态无可避免地必须承担这些运行期成本。

然而,在许多情况下我们需要的仅是可以在编译期决定,无需变动的多态行为。那么一来,奇怪的递归模板样式()便可被用来达成静态多态。如下例:

template <class Derived>struct base{    void interface()    {         // ...         static_cast<Derived*>(this)->implementation();         // ...    }};struct derived : base<derived>{     void implementation();};

这里基类模板有着这样的优点:成员函数的本体在被他们的宣告之前都不会被实体化,而且它可利用 static_cast 并透过自己的函数来使用派生类的成员,所以能够在编译时产生出带有多态特性的对象复合物。在现实使用上,Boost迭代器库便有采用 CRTP 的技法。

其他类似的使用还有"Barton-Nackman trick(英文)",有时候被称作"有限制的模板扩张",共同的功能被可以放在一个基类当中,作为必要的构件使用,以此确保在缩减多余代码时的一致行为。

相关