C++的函数重载

写在前面:
函数重载的重要性不言而明,但是你知道C++中函数重载是如何实现的呢(虽然本文谈的是C++中函数重载的实现,但我想其它语言也是类似的)?这个可以分解为下面两个问题

1、声明/定义重载函数时,是如何解决命名冲突的?(抛开函数重载不谈,using就是一种解决命名冲突的方法,解决命名冲突还有很多其它的方法,这里就不论述了)
2、当我们调用一个重载的函数时,又是如何去解析的?(即怎么知道调用的是哪个函数呢)
这两个问题是任何支持函数重载的语言都必须要解决的问题!带着这两个问题,我们开始本文的探讨。本文的主要内容如下:

 1、例子引入(现象)
什么是函数重载(what)?
为什么需要函数重载(why)?
2、编译器如何解决命名冲突的?
函数重载为什么不考虑返回值类型
3、重载函数的调用匹配
模凌两可的情况
4、编译器是如何解析重载函数调用的?
根据函数名确定候选函数集
确定可用函数
确定最佳匹配函数
5、总结
1、例子引入(现象)
1.1、什么是函数重载(what)?
函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。

When two or more different declarations are specified for a single name in the same scope,  that name is said to overloaded.  By extension, two declarations in the same scope that declare the same name but with different types are called overloaded declarations. Only function declarations can be overloaded; object and type declarations cannot be overloaded. ——摘自《ANSI C++ Standard. P290》

看下面的一个例子,来体会一下:实现一个打印函数,既可以打印int型、也可以打印字符串型。在C++中,我们可以这样做:

#include<iostream>
using namespace std;

void print(int i)
{
        cout<<"print a integer :"<<i<<endl;
}

void print(string str)
{
        cout<<"print a string :"<<str<<endl;
}

int main()
{
        print(12);
        print("hello world!");
        return 0;
}

通过上面代码的实现,可以根据具体的print()的参数去调用print(int)还是print(string)。上面print(12)会去调用print(int),print("hello world")会去调用print(string),如下面的结果:(先用g++ test.c编译,然后执行)

1.2、为什么需要函数重载(why)?
试想如果没有函数重载机制,如在C中,你必须要这样去做:为这个print函数取不同的名字,如print_int、print_string。这里还只是两个的情况,如果是很多个的话,就需要为实现同一个功能的函数取很多个名字,如加入打印long型、char*、各种类型的数组等等。这样做很不友好!
类的构造函数跟类名相同,也就是说:构造函数都同名。如果没有函数重载机制,要想实例化不同的对象,那是相当的麻烦!
操作符重载,本质上就是函数重载,它大大丰富了已有操作符的含义,方便使用,如+可用于连接字符串等!
通过上面的介绍我们对函数重载,应该唤醒了我们对函数重载的大概记忆。下面我们就来分析,C++是如何实现函数重载机制的。

2、编译器如何解决命名冲突的?
为了了解编译器是如何处理这些重载函数的,我们反编译下上面我们生成的执行文件,看下汇编代码(全文都是在Linux下面做的实验,Windows类似,你也可以参考《一道简单的题目引发的思考》一文,那里既用到Linux下面的反汇编和Windows下面的反汇编,并注明了Linux和Windows汇编语言的区别)。我们执行命令objdump -d a.out >log.txt反汇编并将结果重定向到log.txt文件中,然后分析log.txt文件。

发现函数void print(int i) 编译之后为:(注意它的函数签名变为——_Z5printi)

发现函数void print(string str) 编译之后为:(注意它的函数签名变为——_Z5printSs)

 

我们可以发现编译之后,重载函数的名字变了不再都是print!这样不存在命名冲突的问题了,但又有新的问题了——变名机制是怎样的,即如何将一个重载函数的签名映射到一个新的标识?我的第一反应是:函数名+参数列表,因为函数重载取决于参数的类型、个数,而跟返回类型无关。但看下面的映射关系:

void print(int i)                    -->         _Z5printi
void print(string str)         -->         _Z5printSs

进一步猜想,前面的Z5表示返回值类型,print函数名,i表示整型int,Ss表示字符串string,即映射为返回类型+函数名+参数列表。最后在main函数中就是通过_Z5printi、_Z5printSs来调用对应的函数的:

80489bc:       e8 73 ff ff ff          call   8048934 <_Z5printi>
……………
80489f0:       e8 7a ff ff ff          call   804896f <_Z5printSs>

我们再写几个重载函数来验证一下猜想,如:

void print(long l)           -->           _Z5printl
void print(char str)      -->           _Z5printc
可以发现大概是int->i,long->l,char->c,string->Ss….基本上都是用首字母代表,现在我们来现在一个函数的返回值类型是否真的对函数变名有影响,如:

#include<iostream>
using namespace std;

int max(int a,int b)
{
        return a>=b?a:b;
}

double max(double a,double b)
{
        return a>=b?a:b;
}
int main()
{
        cout<<"max int is: "<<max(1,3)<<endl;
        cout<<"max double is: "<<max(1.2,1.3)<<endl;
        return 0;
}

 

int max(int a,int b) 映射为_Z3maxii、double max(double a,double b) 映射为_Z3maxdd,这证实了我的猜想,Z后面的数字代码各种返回类型。更加详细的对应关系,如那个数字对应那个返回类型,哪个字符代表哪重参数类型,就不去具体研究了,因为这个东西跟编译器有关,上面的研究都是基于g++编译器,如果用的是vs编译器的话,对应关系跟这个肯定不一样。但是规则是一样的:“返回类型+函数名+参数列表”。

既然返回类型也考虑到映射机制中,这样不同的返回类型映射之后的函数名肯定不一样了,但为什么不将函数返回类型考虑到函数重载中呢?——这是为了保持解析操作符或函数调用时,独立于上下文(不依赖于上下文),看下面的例子

float sqrt(float);
double sqrt(double);

void f(double da, float fla)
{
      float fl=sqrt(da);//调用sqrt(double)
      double d=sqrt(da);//调用sqrt(double)

      fl=sqrt(fla);//调用sqrt(float)
   d=sqrt(fla);//调用sqrt(float)
}

如果返回类型考虑到函数重载中,这样将不可能再独立于上下文决定调用哪个函数。

至此似乎已经完全分析清楚了,但我们还漏了函数重载的重要限定——作用域。上面我们介绍的函数重载都是全局函数,下面我们来看一下一个类中的函数重载,用类的对象调用print函数,并根据实参调用不同的函数:

#include<iostream>
using namespace std;

class test{
public:
        void print(int i)
        {
                cout<<"int"<<endl;
        }
        void print(char c)
        {
                cout<<"char"<<endl;
        }
};
int main()
{
        test t;
        t.print(1);
        t.print('a');
        return 0;
}

我们现在再来看一下这时print函数映射之后的函数名:

void print(int i)                    -->            _ZN4test5printEi

void print(char c)               -->            _ZN4test5printEc

注意前面的N4test,我们可以很容易猜到应该表示作用域,N4可能为命名空间、test类名等等。这说明最准确的映射机制为:作用域+返回类型+函数名+参数列表

3、重载函数的调用匹配
现在已经解决了重载函数命名冲突的问题,在定义完重载函数之后,用函数名调用的时候是如何去解析的?为了估计哪个重载函数最适合,需要依次按照下列规则来判断:

精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、T到const T;
提升匹配:即整数提升(如bool 到 int、char到int、short 到int),float到double
使用标准转换匹配:如int 到double、double到int、double到long double、Derived*到Base*、T*到void*、int到unsigned int;
使用用户自定义匹配;
使用省略号匹配:类似printf中省略号参数
如果在最高层有多个匹配函数找到,调用将被拒绝(因为有歧义、模凌两可)。看下面的例子:

void print(int);
void print(const char*);
void print(double);
void print(long);
void print(char);

void h(char c,int i,short s, float f)
{
     print(c);//精确匹配,调用print(char)
     print(i);//精确匹配,调用print(int)
     print(s);//整数提升,调用print(int)
     print(f);//float到double的提升,调用print(double)

     print('a');//精确匹配,调用print(char)
     print(49);//精确匹配,调用print(int)
     print(0);//精确匹配,调用print(int)
     print("a");//精确匹配,调用print(const char*)
}

定义太少或太多的重载函数,都有可能导致模凌两可,看下面的一个例子:

void f1(char);
void f1(long);

void f2(char*);
void f2(int*);

void k(int i)
{
       f1(i);//调用f1(char)? f1(long)?
       f2(0);//调用f2(char*)?f2(int*)?
}

这时侯编译器就会报错,将错误抛给用户自己来处理:通过显示类型转换来调用等等(如f2(static_cast<int *>(0),当然这样做很丑,而且你想调用别的方法时有用做转换)。上面的例子只是一个参数的情况,下面我们再来看一个两个参数的情况:

这个四个解析步骤所做的事情大致如下:

由匹配文法中的函数调用,获取函数名;
获得函数各参数表达式类型;
语法分析器查找重载函数,符号表内部经过重载解析返回最佳的函数
语法分析器创建抽象语法树,将符号表中存储的最佳函数绑定到抽象语法树上
 

下面我们重点解释一下重载解析,重载解析要满足前面《3、重载函数的调用匹配》中介绍的匹配顺序和规则。重载函数解析大致可以分为三步:

根据函数名确定候选函数集
从候选函数集中选择可用函数集合
从可用函数集中确定最佳函数,或由于模凌两可返回错误
4.1、根据函数名确定候选函数集
根据函数在同一作用域内所有同名的函数,并且要求是可见的(像private、protected、public、friend之类)。“同一作用域”也是在函数重载的定义中的一个限定,如果不在一个作用域,不能算是函数重载,如下面的代码:

void f(int);

void g()
{
        void f(double);
        f(1); //这里调用的是f(double),而不是f(int)
}

 

即内层作用域的函数会隐藏外层的同名函数!同样的派生类的成员函数会隐藏基类的同名函数。这很好理解,变量的访问也是如此,如一个函数体内要访问全局的同名变量要用“::”限定。

为了查找候选函数集,一般采用深度优选搜索算法:

step1:从函数调用点开始查找,逐层作用域向外查找可见的候选函数
step2:如果上一步收集的不在用户自定义命名空间中,则用到了using机制引入的命名空间中的候选函数,否则结束

在收集候选函数时,如果调用函数的实参类型为非结构体类型,候选函数仅包含调用点可见的函数;如果调用函数的实参类型包括类类型对象、类类型指针、类类型引用或指向类成员的指针,候选函数为下面集合的并:

(1)在调用点上可见的函数;
(2)在定义该类类型的名字空间或定义该类的基类的名字空间中声明的函数;
(3)该类或其基类的友元函数;
下面我们来看一个例子更直观:

void f();
void f(int);
void f(double, double = 314);
names pace N
{ 
    void f(char3 ,char3);
}
classA{
    public: operat or double() { }
};
int main ( )
{
    using names pace N; //using指示符
    A a;
    f(a);
    return 0;
}

根据上述方法,由于实参是类类型的对象,候选函数的收集分为3步:

(1)从函数调用所在的main函数作用域内开始查找函数f的声明, 结果未找到。到main函数
作用域的外层作用域查找,此时在全局作用域找到3个函数f的声明,将它们放入候选集合;

(2)到using指示符所指向的命名空间 N中收集f ( char3 , char3 ) ;

(3)考虑2类集合。其一为定义该类类型的名字空间或定义该类的基类的名字空间中声明的函
数;其二为该类或其基类的友元函数。本例中这2类集合为空。

最终候选集合为上述所列的 4个函数f。

4.2、确定可用函数
可用的函数是指:函数参数个数匹配并且每一个参数都有隐式转换序列。

(1)如果实参有m个参数,所有候选参数中,有且只有 m个参数;
(2)所有候选参数中,参数个数不足m个,当前仅当参数列表中有省略号;
(3)所有候选参数中,参数个数超过 m个,当前仅当第m + 1个参数以后都有缺省值。如果可用
集合为空,函数调用会失败。
这些规则在前面的《3、重载函数的调用匹配》中就有所体现了。

4.3、确定最佳匹配函数
确定可用函数之后,对可用函数集中的每一个函数,如果调用函数的实参要调用它计算优先级,最后选出优先级最高的。如对《3、重载函数的调用匹配》中介绍的匹配规则中按顺序分配权重,然后计算总的优先级,最后选出最优的函数。

5、总结
本文介绍了什么是函数重载、为什么需要函数重载、编译器如何解决函数重名问题、编译器如何解析重载函数的调用。通过本文,我想大家对C++中的重载应该算是比较清楚了。说明:在介绍函数名映射机制是基于g++编译器,不同的编译器映射有些差别;编译器解析重载函数的调用,也只是所有编译器中的一种。如果你对某个编译器感兴趣,请自己深入去研究。

最后我抛给大家两个问题:

1、在C++中加号+,即可用于两个int型之间的相加、也可以用于浮点数数之间的相加、字符串之间的连接,那+算不算是操作符重载呢?换个场景C语言中加号+,即可用于两个int型之间的相加、也可以用于浮点数数之间的相加,那算不算操作符重载呢?
2、模板(template)的重载时怎么样的?模板函数和普通函数构成的重载,调用时又是如何匹配的呢?
附录:一种C++函数重载机制
这个机制是由张素琴等人提出并实现的,他们写了一个C++的编译系统COC++(开发在国产机上,UNIX操作系统环境下具有中国自己版权的C、C++和FORTRAN语言编译系统,这些编译系统分别满足了ISOC90、AT&T的C++85和ISOFORTRAN90标准)。COC++中的函数重载处理过程主要包括两个子过程:

1、在函数声明时的处理过程中,编译系统建立函数声明原型链表,按照换名规则进行换名并在函数声明原型链表中记录函数换名后的名字(换名规则跟本文上面描述的差不多,只是那个int-》为哪个字符、char-》为哪个字符等等类似的差异)

 

图附1、过程1-建立函数链表(说明,函数名的编码格式为:<原函数名>_<作用域换名><函数参数表编码>,这跟g++中的有点不一样)

2、在函数调用语句翻译过程中,访问符号表,查找相应函数声明原型链表,按照类型匹配原则,查找最优匹配函数节点,并输出换名后的名字下面给出两个子过程的算法建立函数声明原型链表算法流程如图附1,函数调用语句翻译算法流程如图附2。

 

附-模板函数和普通函数构成的重载,调用时又是如何匹配的呢?

下面是C++创始人Bjarne Stroustrup的回答:

#include <iostream> // std::cout

// A CRTP base class for Vecs with a size and indexing:
template <typename E>
class VecExpr {
public:
    double operator[](int i) const { return static_cast<E const&>(*this)[i]; }
    operator E const&() const { return static_cast<const E&>(*this); } // 向下类型转换
};
// The actual Vec class:
class Vec : public VecExpr<Vec> {
    double _data[1000];
public:
    double&  operator[](int i) { return _data[i]; }
    double operator[](int i) const { return _data[i]; }
    template <typename E>
    Vec const& operator=(VecExpr<E> const& vec) {
        E const& v = vec;
        for (int i = 0; i<1000; ++i) _data[i] = v[i];
        return *this;
    }
    // Constructors
    Vec() { }
    Vec(double v) { for(int i=0; i<1000; ++i) _data[i] = v; }
};

template <typename E1, typename E2>
class VecDifference : public VecExpr<VecDifference<E1, E2> > {
    E1 const& _u; E2 const& _v;
public:
    VecDifference(VecExpr<E1> const& u, VecExpr<E2> const& v) : _u(u), _v(v) { }
    double operator[](int i) const { return _u[i] - _v[i]; }
};
template <typename E>
class VecScaled : public VecExpr<VecScaled<E> > {
    double _alpha; E const& _v;
public:
    VecScaled(double alpha, VecExpr<E> const& v) : _alpha(alpha), _v(v) { }
    double operator[](int i) const { return _alpha * _v[i]; }
};

// Now we can overload operators:
template <typename E1, typename E2> VecDifference<E1, E2> const
operator-(VecExpr<E1> const& u, VecExpr<E2> const& v) {
    return VecDifference<E1, E2>(u, v);
}
template <typename E> VecScaled<E> const
operator*(double alpha, VecExpr<E> const& v) {
    return VecScaled<E>(alpha, v);
}

int main() {
    Vec u(3), v(1); double alpha=9; Vec y;
    y = alpha*(u - v);
    std::cout << y[999] << '\n';
    std::cin.get(); return 0;
}

 

其实,C++ 标准定义了类似的 traits:std::iterator_trait(另一个经典例子是 std::numeric_limits) 。特性对类型的信息(如 value_type、 reference)进行包装,使得上层代码可以以统一的接口访问这些信息。C++ 模板元编程会涉及大量的类型计算,很多时候要提取类型的信息(typedef、 常量值等),如果这些类型的信息的访问方式不一致(如上面的迭代器和指针),我们将不得不定义特例,这会导致大量重复代码的出现(另一种代码膨胀),而通过加一层特性可以很好的解决这一问题。另外,特性不仅可以对类型的信息进行包装,还可以提供更多信息,当然,因为加了一层,也带来复杂性。特性是一种提供元信息的手段。

策略(policy)一般是一个类模板,典型的策略是 STL 容器(如 std::vector<>,完整声明是template<class T, class Alloc=allocator<T>> class vector;)的分配器(这个参数有默认参数,即默认存储策略),策略类将模板的经常变化的那一部分子功能块集中起来作为模板参数,这样模板便可以更为通用,这和特性的思想是类似的(详见文献[1] 12.3)。

标签(tag)一般是一个空类,其作用是作为一个独一无二的类型名字用于标记一些东西,典型的例子是 STL 迭代器的五种类型的名字(input_iterator_tag, output_iterator_tag, forward_iterator_tag, bidirectional_iterator_tag, random_access_iterator_tag),std::vector<int>::iterator::iterator_category 就是 random_access_iterator_tag,可以用第1节判断类型是否等价的模板检测这一点:

#include <iostream>
#include <vector>

template<typename T1, typename T2> // 通例,返回 false
class theSameType       { public: enum { ret = false }; };
template<typename T>               // 特例,两类型相同时返回 true
class theSameType<T, T> { public: enum { ret = true }; };

int main(){
    std::cout << theSameType< std::vector<int>::iterator::iterator_category,
                              std::random_access_iterator_tag >::ret << '\n';
    std::cin.get(); return 0;
}
1

C++11 引入了变长模板参数,其背后的原理也是模板递归(文献[1]第 230 页)。

利用和上面例子类似的模板参数移位递归的原理,我们可以构造一个存储“类型”的元组,即元容器,其代码如下(和文献[1]第 237 页的例子不同):

#include <iostream>

// 元容器
template<typename T0=void, typename T1=void, typename T2=void, typename T3=void>
class meta_container {
public:
    typedef T0 type;
    typedef meta_container<T1, T2, T3, void> next_node; // 参数移位了
    static const int size = next_node::size + 1; // 非 null_type 模板参数个数
};
template<> // 特例,递归终止
class meta_container<void, void, void, void> {
public:
    typedef void type;
    static const int size = 0;
};

// 访问元容器中的数据
template<typename C, unsigned i>
class get {
public:
    static_assert(i<C::size, "get<C,i>: index exceed num"); // C++11 引入静态断言
    typedef typename get<C,i-1>::c_type::next_node c_type;
    typedef typename c_type::type ret_type;
};
template<typename C>
class get<C, 0> {
public:
    static_assert(0<C::size, "get<C,i>: index exceed num"); // C++11 引入静态断言
    typedef C c_type;
    typedef typename c_type::type ret_type;
};

// 在元容器中查找某个类型,找到返回索引,找不到返回 -1
template<typename T1, typename T2> class same_type { public: enum { ret = false }; };
template<typename T> class same_type<T, T> { public: enum { ret = true }; };

template<bool c, typename Then, typename Else> class IF_ { };
template<typename Then, typename Else>
class IF_<true, Then, Else> { public: typedef Then reType; };
template<typename Then, typename Else>
class IF_<false, Then, Else> { public: typedef Else reType; };

template<typename C, typename T>
class find {
    template<int i> class number { public: static const int ret = i; };
    template<typename C, typename T, int i>
    class find_i {
    public:
        static const int ret = IF_< same_type<get<C,i>::ret_type, T>::ret,
            number<i>, find_i<C,T,i-1> >::reType::ret;
    };
    template<typename C, typename T>
    class find_i<C, T, -1> {
    public:
        static const int ret = -1;
    };
public:
    static const int ret = find_i<C, T, C::size-1>::ret;
};

int main(){
    typedef meta_container<int, int&, const int> mc;
    int a = 9999;
    get<mc, 1>::ret_type aref = a;
    std::cout << mc::size << '\n';
    std::cout << aref << '\n';
    std::cout << find<mc, const int>::ret << '\n';
    std::cout << find<mc, float>::ret << '\n';
    std::cin.get(); return 0;
}
3
9999
2
-1

上面例子已经实现了存储类型的元容器,和元容器上的查找算法,但还有一个小问题,就是它不能处理模板,编译器对模板的操纵能力远不如对类型的操纵能力强(提示:类模板实例是类型),我们可以一种间接方式实现存储“模板元素”,即用模板的一个代表实例(如全用 int 为参数的实例)来代表这个模板,这样对任意模板实例,只需判断其模板的代表实例是否在容器中即可,这需要进行类型过滤:对任意模板的实例将其替换为指定模板参数的代表实例,类型过滤实例代码如下(参考了文献[1]第 241 页):

// 类型过滤,meta_filter 使用时只用一个参数,设置四个模板参数是因为,模板通例的参数列表
// 必须能够包含特例参数列表,后面三个参数设置默认值为 void 或标签模板
template<typename T> class dummy_template_1 {};
template<typename T0, typename T1> class dummy_template_2 {};
template<typename T0, typename T1 = void,
    template<typename> class tmp_1 = dummy_template_1,
    template<typename, typename> class tmp_2 = dummy_template_2>
class meta_filter { // 通例,不改变类型
public:
    typedef T0 ret_type;
};
                    // 匹配任何带有一个类型参数模板的实例,将模板实例替换为代表实例
template<template<typename> class tmp_1, typename T>
class meta_filter<tmp_1<T>, void, dummy_template_1, dummy_template_2> {
public:
    typedef tmp_1<int> ret_type;
};
                    // 匹配任何带有两个类型参数模板的实例,将模板实例替换为代表实例
template<template<typename, typename> class tmp_2, typename T0, typename T1>
class meta_filter<tmp_2<T0, T1>, void, dummy_template_1, dummy_template_2> {
public:
    typedef tmp_2<int, int> ret_type;
};

现在,只需将上面元容器和元容器查找函数修改为:对模板实例将其换为代表实例,即修改 meta_container<> 通例中“typedef T0 type;”语句为“typedef typename meta_filter<T0>::ret_type type;”,修改 find<> 的最后一行中“T”为“typename meta_filter<T>::ret_type”。修改后,下面代码的执行结果是:

template<typename, typename> class my_tmp_2;

// 自动将 my_tmp_2<float, int> 过滤为 my_tmp_2<int, int>
typedef meta_container<int, float, my_tmp_2<float, int>> mc2;
// 自动将 my_tmp_2<char, double> 过滤为 my_tmp_2<int, int>
std::cout << find<mc2, my_tmp_2<char, double>>::ret << '\n'; // 输出 2
2

10. 总结

博文比较长,总结一下所涉及的东西:

C++ 模板包括函数模板和类模板,模板参数形式有:类型、模板型、非类型(整型、指针);
模板的特例化分完全特例化和部分特例化,实例将匹配参数集合最小的特例;
用实例参数替换模板形式参数称为实例化,实例化的结果是产生具体类型(类模板)或函数(函数模板),同一模板实参完全等价将产生等价的实例类型或函数;
模板一般在头文件中定义,可能被包含多次,编译和链接时会消除等价模板实例;
template、typename、this 关键字用来消除歧义,避免编译错误或产生不符预期的结果;
C++11 对模板引入了新特性:“>>”、函数模板也可以有默认参数、变长模板参数、外部模板实例(extern),并弃用 export template;
C++ 模板是图灵完备的,模板编程是函数编程风格,特点是:没有可变的存储、递归,以“<>”为输入,typedef 或静态常量为输出;
编译期数值计算虽然实际意义不大,但可以很好证明 C++ 模板的能力,可以用模板实现类似普通程序中的 if 和 while 语句;
一个实际应用是循环展开,虽然编译器可以自动循环展开,但我们可以让这一切更可控;
C++ 模板编程的两个问题是:难调试,会产生冗长且难以阅读的编译错误信息、代码膨胀(源代码膨胀、二进制对象文件膨胀),改进的方法是:增加一些检查代码,让编译器及时报错,使用特性、策略等让模板更通用,可能的话合并一些模板实例(如将代码提出去做成单独模板);
表达式模板和向量计算是另一个可加速程序的例子,它们将计算表达式编码到类型,这是通过模板嵌套参数实现的;
特性,策略,标签是模板编程常用技巧,它们可以是模板变得更加通用;
模板甚至可以获得类型的内部信息(是否有某个 typedef),这是反射中的内省,C++ 在语言层面对反射支持很少(typeid),这不利于模板元编程;
可以用递归实现伪变长参数模板,C++11 变长参数模板背后的原理也是模板递归;
元容器存储元信息(如类型)、类型过滤过滤某些类型,它们是元编程的高级特性。
 

进一步学习

C++ 确实比较复杂,这可能是因为,虽然 C++ 语言层次比较低,但它却同时可以实现很多高级特性。进一步学习 C++ 模板元编程的途径很多:

C++ 标准库的 STL 可能是最好的学习案例,尤其是其容器、迭代器、通用算法、函数类模板等部件,实现机制很巧妙;
另外一个 C++ 库也值得一看,那就是 Boost 库,Boost 的元编程库参考文献[16];
很推荐《深入实践C++模板编程》这本书,这篇博文大量参考了这本书;
wikibooks.org 上有个介绍 C++ 各种编程技巧书:More C++ Idioms,文献[15];
文献[17]列了 C++ 模板的参考书,共四本;
好多东西,书上讲的比较浅显,而且不全面,有时候直接看 C++ 标准(最新 C++11)可能更为高效,C++ 标准并不是想象中那样难读,C++ 标准委员会网站的 Papers 也很值得看,文献[3]。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值