Coding之路——重新学习C++(8):神奇的模板

1.解析一个正确的模板类

  (1)首先,我们想创造一个模板,可以先针对一个特定的类型参数设计它的行为方式,然后在对抽象的一般类型进行推广。例如我们可以先设计String<char>类的具体实现,然后再推广到String<C>类模板。

  (2)类模板的名字是不能重载的。所以,如果在某个作用域内声明了一个类模板,就不能有其他同样名字的实体了。

template<class T> class String{/*...*/};
class String {/*...*/};                                //错误:重复定义

  (3)在我们给模板实例化的时候,我们仅仅需要生成代码中我们用到的模板实例函数代码:

String<char> cs;

void f(){
    String <Jchar> js;
    cs = "Hello Word!";
}

这段代码中,只生成了String<char>和String<Jchar>的声明,和与它们对应的Srep类型,默认构造函数和析构函数,还有String<char>::operator=(char *),其他成员函数没有被使用,就不被生成。 

  (4)模板参数。模板参数可以使常量表达式,具有外部连接的对象或者函数地址,或者非重载的指向成员的指针。用做模板参数的指针必须具有&of的形式,其中of必须是对象或者函数的名字。到成员的指针必须有&X::of的形式,of是一个成员名。特别的,字符串常量不能作为模板参数。另外,一般把整型参数用于提供大小或界限:

template<class T, int i>class Buffer{
    T v[i];
    //...
};

  (5)类型检查。在模板定义里的名字必须在作用域里,或者以合理的明确的方式依赖于模板参数。另外,模板参数使用上的错误只有在模板使用时才能被检查出来。

2.函数模板

  (1)类型的自动推断。对于一个函数模板来说,编译器能够能够从一个调用中推断出类型参数和非类型参数,条件是函数调用的参数列表能唯一的标识类型参数的集合。但是编译器绝对不会对模板类自动推断类型,因为一个类可能有多个构造函数,这样自动推断就会不清楚具体的模板参数类型。在模板函数中经常会使用显式描述,显式描述的最常见用途是为模板函数提供返回值:

template<class T, class U> T implict_cast(U u){return u;}
int i = 1;
implict_cast<double>(i);           //T是double, U是int
impict_cast<char, double>(i);   //T是char,U是double

  (2)模板函数的解析规则,我们用一个具体的例子来理解:

template<class T> T sqrt(T t);
template<class T> complex<T> sqrt(complex<T> t);
double sqrt(double b);

complex<double> z;
sqrt(2);        //sqrt<int>(int)
sqrt(2.0);     //sqrt(double)
sqrt(z);        //sqrt<double>(complex<double>)

——找出能参与这个重载解析的一组函数模板的专门化。例如sqrt(z),产生出sqrt<double>(complex<double>)和sqrt<complex<double> >(complex<double>)。

——选择其中专门化程度最高的函数模板。这就意味着sqrt(z)选择sqrt<double>(complex<double>)。因为任何匹配sqrt<T>(complex<T>)都匹配sqrt<T>(T)。

——做重载解析,应该同时考虑常规函数。如果一个模板参数已经被自动推断出来,则不能通过类型转换来匹配常规函数。对于sqrt(2)来说,选择sqrt<int>(int)而不是           sqrt(double)。

——如果一个函数和一个专门化同样好,那么选择函数。sqrt(2.0)选择了sqrt(double)而不是sqrt<double>(double)。

——找不到匹配就是一个错误。要么通过显式调用消除歧义,要么增加适当的声明。

3.怎样用模板参数描述策略

  我们先来看一个排序实例。我们使用的排序规则基本一致,但是假如瑞典人的姓名排序就有些特殊, 这样我们就得另外制定一个排序规则,将它运用到函数模板中:

template<class T> class Cmp{
    static int eq(T a, T b){return a == b;}
    static int lt(T a, T b){return a < b;}
    static int gt(T a, T b){return a > b;}
};

class Literate{
       static int eq(char a, char b){return a==b;}
       static int lt(char, char){//...}               //基于字符值查一个表
};

template<class T, class C = Cmp<T> >
int compare(const String<T> &a, const String<T> &b){
     for(int i = 0;i < a.length() && i < b.length();i++){
        if(!C::eq(a[i], b[i])) return C::lt(a[i], b[i])? -1 : 1;
    return a.length() - b.length();
}

//调用
void f(String<char> a, String<char> b){
    compare(a, b);                  //用Cmp<char>
    compare<char, Literate>; //用Literate
}

在示例中,把比较操作作为模板参数传递有两个优点:一是可以通过一个参数传递几个操作,二是没有运行时的开销。

4.专门化

  在模板中,针对不同的模板参数我们可能需要不同的实现方式,这时我们就需要专门化来为我们制定特定类型的模板的实现方式。专门化针对共同界面,为不同的参数提供不同的实现方法。注意,模板本身和专门化必须在同一个命名空间中,通用模板必须在专门化模板之前声明,使用专门化的调用也必须在专门化模板的作用域。

下面这个例子就实现了对指针容器的专门化:

//通用模板
template<class T> class Vector{/*...*/};
//针对void*的模板专门化
template<> class Vector<void*>{
    void **p;
    //...
    void* operator[](int i );
};
//针对T*的部分专门化
template<class T> class Vector<T*>:private Vector<void*>{
public:
    typedef  Vector<void*> Base;
    Vector():Base(){}
    explicit Vector(int i):Base(i){}
    T*& elem(int i){return reinterpret_cast<T*&>(Base::elem(i));}
    T*& operator[](int i){
        return reinterpret_cast<T*&>(Base::operaotr[](i));
    }
};

从一个非模板类派生出模板类,这是为一组模板提供共同实现的一种方法。

5.派生与模板

  (1)我们实现一个容器类,那如何将容器与操作分离呢,第一种方法是通过继承,父类模板参数类型是子类:

template<class T> class Base_ops{
public:
    bool operator==(const T&) const;
    bool operator!=(const T&) const;
    //给T操作访问的权限
    const T& derived() const{return static_cast<const T&>(*this);}
    //...
};

template<class T> Math_container:
    public Base_ops<Math_container<T> >{
    public:
        size_t size() const;
        T& operator[](size_t i);
        const T& operator[](size_t i) const;
        //...
};

第二种方法是将容器和操作通过末班参数组合起来:

template<class T, class C> class Mcontainer{
    C elements;
public:
    T& operator[](size_t i){return elements[i];}
    friend bool operator==(const Mcontainer&, const Mcontainer&);
    friend bool operator!=(const Mcontainer&, const Mcontainer&);
};

template<class T> class My_array{/*...*/};
Mcontainer<double, My_array<double> > mc;

  (2)一个类可以包含本身就是模板的成员:

template<class S>class complex{
    S re, im;
    template<class T>
        complex(const complex<T> &c): re(c.real()),im(c.imag()){}
    //...
};

complex<float> f(0.0);
complex<double> d = f; //可以:有float到double的转换

但是模板构造函数不会用于生成复制构造函数和赋值运算符,所以必须自己定义复制构造函数和赋值运算符。

  (3)同一个模板生成的两个类之间不存在任何关系。

class Shape{/*...*/};
class Circle:public Shape{/*...*/};
class Triangle:public Shape{/*...*/};

void f(set<Shape*> &s){
    s.insert(new Triangle());
}

void g(set<Circle*> &s){
    f(s);            //错误:类型呢不匹配,set<Circle*>不是set<Shape*>
}

我们必须保证set<Circle*>的成员一定是Circle,如果将set<Circle*>看成set<Shape*>,我们将不能得到保证,因为set<Shape*>允许插入Triangle类型,如果set<Shape*>实际是一个set<Circle*>,那么我们就摧毁了set<Circle*>的成员一定是Circle的基本保证。

时间: 2024-10-13 22:41:55

Coding之路——重新学习C++(8):神奇的模板的相关文章

Coding之路——重新学习C++(3):对于编译和链接的重新认识

1.C++的源代码是怎么变成程序的. (1)我们在编写完源代码后,首先需要把源代码交给编译器,编译器首先进行预处理,也就是处理宏,把#include指令引进的头文件全部引进,产生编译单元.编译单元是编译器的真正工作对象,是真正意义上的C++对象. (2)一般的编译模式会采用分别编译,这时我们必须保证所有的声明具有一致性,连接器程序帮助我们把所有编译的部分都约束在一起,让所有的对象.函数没有二义性,这些都在程序运行前结束.当然,也有可以再程序运行后加入新代码(动态连接). 2.连接时的二三事(必须

Coding之路——重新学习C++(4):定义一个正确的类

我们都能定义一个类,可是如何定义一个正确的类,这是一个需要我们深入理解的问题.C++之父曾经说过定义新类型的基本思想就是将实现一个类的时候并非必要的细节(存储该类型的对象采用的布局细节)和对于这个类的正确使用至关重要的性质(访问数据的成员函数)分开设计.这种区分的最好实现方式是提供一个特定的表层接口,所有对于类内部数据结构和内部维护的调用都通过这个表层接口. 1.类该怎么定义 (1)首先我们要明白,建立一个对象,构造函数把成员变量都放在了堆之中(除了static变量之外,static变量放在全局

Coding之路——重新学习C++(2):static的详细理解

一.C中的static关键字 1. static 局部变量 静态局部变量属于静态存储方式,它具有以下特点: (1)静态局部变量 在函数内定义它的生存期为 整个程序生命周期,但是其 作用域仍与 自动变量相同 ,只能在定义该变量的函数内使用该变量.退出该函数后,尽管该变量还继续存在,但不能使用它.(2)对基本类型的静态局部变量若在声明时未赋以初值,则系统自动赋予0值 .而对自动变量不赋初值,则其值是不定的. 根据静态局部变量的特点,可以看出它是一种生存期为整个程序生命周期.虽然离开定义它的函数后不能

Coding之路——重新学习C++(10):类的层次结构

1.多重继承 (1)多重继承一直是C++中让许多人诟病的机制,不过它大大增加了类的层次结构的灵活性,先看一个简单的例子: class Task{ public: virtual void pending() = 0; //... }; class Displayed{ public: //... virtual void draw() = 0; }; class Satelite:public Task, public Displayed{ public: //... void pending(

Coding之路——重新学习C++(9):解决异常

1.什么是异常 (1)异常的基本思路是让一个函数发现自己无法解决的错误时抛出异常,让调用者来解决.异常处理机制类似于编译时的类型检查和歧义性控制在运行时的对应物,它是一种非局部的控制结构,在抛出异常时,我们用堆栈回退来找到能处理异常的上层函数.有人把异常想象成程序中那些无法挽回的重大错误,但是异常通常代表的是“系统的某些部分不能完成要它做的事”. (2)在我们用catch捕捉异常的时候,我们用实际参数的值对形参初始化,这可能造成我们抛出的异常因为捕捉而被切割,所以我们经常用指针和引用捕捉异常来保

Coding之路——重新学习C++(7):用继承写出一个好类

1.继承类时需要注意的地方 (1)当一个类作为基类的时候,这个类就必须有定义. (2)在派生类中,基类的默认构造函数可以被隐式调用,但是如果基类的构造函数都有参数,派生类需要直接调用一个.派生类的构造函数只能描述派生类自己的成员变量和自己的基类的直接初始式,它不能直接初始化基类的成员. Manager::Manager(const string &n, int d, int lvl) :family_name(n), //错误:在Manager里没有family_name声明 departmen

Coding之路——重新学习C++(1):C++基础知识盲点总结

最近为了找工作参加了许多公司的笔试和面试,发现了以前的知识虽然学了很多,但是并不深入和系统.所以准备把一些书重新读一读,并且打算做一些总结,毕竟老祖宗教导我们“学而时习之,不亦说乎”. 1.把程序分成模块 当我们做程序一般都是分成许多模块去做,因为这样可以保证模块之间的独立性,不会因为一个模块的改动影响整个程序.所以我们在分模块的时候最重要的就是在一系列有关的过程(函数)和它们用到的数据组你开织在一起,在C++中一般放入一个命名空间中或者一个类中.每个模块应该提供接口隐藏数据和功能函数的具体实现

Coding之路——重新学习C++(6):一个String类

这个String类是对运算符重载和以前知识的一个综合应用,提供了值语义.字符串读写.检查和不检查的访问.流I/O和字符串拼接等功能. 1.String类的定义 class String{ //类型的定义 struct Srep; //表示能被几个同样值的String共享 Srep *rep; public: class Cref; //实现下标运算,区别对待读操作和写操作 class Range(); //范围错误时抛出的异常 //构造.赋值和析构函数 String(); //x = "&quo

云计算之路-阿里云上:神奇的“黑色30秒”再次出现,究竟是谁的错?

自从4月28日我们从ASP.NET线程的角度对"黑色30秒"问题进行分析之后,我们采用了新的线程设置,然后观察"黑色30秒"是否再次出现. <processModel enable="true" requestQueueLimit="5000" maxWorkerThreads="100" maxIoThreads="100" minWorkerThreads="50&q