C++泛型与模板

来源:互联网 发布:clover四叶草软件 编辑:程序博客网 时间:2024/06/02 06:46
一、模板定义:
1. 模板形参表不能为空。

2. 模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。非类型形参跟在类型说明符之后声明。

3. 使用函数模板时,可以由编译器去推导出实际模板实参;但是使用类模板时,必须为模板形参显式指定实参。

4. 模板形参遵循常规名字屏蔽规则。与全局作用域中声明的对象、函数或类型同名的模板形参会屏蔽全局名字。

5. 模板类型形参可以用于指定返回类型或函数形参类型,以及在函数体中用于变量声明或强制类型转换。

6.在函数模板形参表中,关键字typename和class具有相同的含义,可以互换使用,但是,关键字typename是作为C++的组成部分加入到C++中,因此旧的程序更有可能只用关键字class。

7.如果要在函数模板内部使用类中定义的类型成员,必须告诉编译器我们正在使用的名字指的是一个类型。默认情况下,编译器假定这样的名字指定数据成员,而不是类型成员。
template <class Parm, classU>
Parm fun(Parm * array, U value)
{
    typename Parm::size_type* p;  // typename不可缺少
                      // 否则size_type被编译器认为
                      // 是一个数据成员的名字。
}

8. 模板费类型形参是模板定义内部的常量值,在需要常量表达式的时候,可使用非类型形参指定数组长度:
template <class T, size_tN>
void array_init(T (& parm)[N])
{
}
参数是一个N个T类型元素数组的引用,函数形参中数组长度必须固定,通过模板非类型形参N,使函数形参数组长度可变。
int x[42];
double y[10];
array_init(x); // array_init(int (&parm)[42])
array_init(y); // array_init(double (&parm)[10])

9. 对模板的非类型形参,求值结果相同的表达式将认为是等价的。

10. 编写泛型代码的重要原则:
模板的形参是const引用;
函数体中的测试只用<比较;

二、实例化:
1. 类模板在引用实际模板类类型时实例化,函数模板在调用它或用它对函数指针进行初始化或赋值时实例化。

2. 类模板形参是必须的,想要使用类模板,必须显式指定模板实参。

3. 使用函数模板时,编译器通常会为我们推断模板实参。

4. 模板实参推断:
(1)多个类型形参的实参必须完全匹配;如果推断的类型不能完全匹配,则调用会出错,模板实参推断失败。
template <typename T>
int compare(const T & v1, const T& v2) {}
compare(3.14, 3); // 无法完全匹配,模板实参推断失败
(2)一般而言,不会转换实参以匹配已有的实例化。接收const引用或const指针的函数可以分别用非const对象的引用或指针来调用,无须产生洗的实例化。无论传递const或非const对象给接收非引用类型的函数,都使用相同的实例化。如果模板形参不是引用类型,则数组实参将当做指向其第一个元素的指针,函数实参当做指向函数类型的指针。
template <typename T>
T fobj(T, T);
template <typename T>
T fref(const T &, const T&);
int a[10], b[42];
fobj(a, b); // fobj(int *, int *);数组大小不同无关紧要
fref(a, b); // error,模板形参为引用类型,无法转换
(3)类型转换的限制只适用于类型为模板形参的那些实参,用普通类型定义的形参可以使用常规转换。
template <typename T>
T sum(const T & op1, int op2);
(4)可以使用函数模板对函数指针进行初始化或赋值,编译器使用指针的类型实例化具有适当模板实参的模板版本。
template <typename T>
int compare(const T &, const T&);
int (* pf1)(const int &, const int&) = compare;
如果不能从函数指针类型确定模板实参,将产生编译时或链接时错误:
void func(int (*)(const string &, const string&));
void func(int (*)(const int &, const int&));
func(compare); // error,无法确定是哪一个实例化

5.某些情况下,不可能推断模板实参的类型,当函数的返回类型必须与形参表中所用的所有类型都不同时,最常出现这一问题。
template <typename T, typenameU>
??? sum(T, U);
希望返回类型足够大,可以包含任意次序传递的任意两个类型的两个值的和。
解决方案是:强制sum的调用者将较小的类型强制转换为希望作为结果使用的类型。
int i; short s;
sum(static_cast<int>(s),i);

6. 指定返回类型的一种方式是引入第三个模板参数:
template <class T1, class T2, classT3>
T1 sum(T2, T3);
对于T1,由于没有实参的类型可用于推断T1的类型,因此,必须每次调用sum时为该形参显式提供实参:
long val = sum<long>(i, lng); //ok: long sum(int, long);
显式模板实参从左到右进行匹配,只有“最右边”的形参的显式模板实参可以忽略:
template <class T1, class T2, classT3>
T2 sum(T1, T3);
// calls long sum(int, long);
long val = sum<int, long,long>(i, lng);

7. 显式实参与函数模板指针:
void func(int (*)(const string &, const string&));
void func(int (*)(const int &, const int&));
func(compare<int>); // ok

三、模板编译模型:
1.一般而言,当调用函数的时候,编译器只需要看到函数的声明。定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的。因此,应该将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。

2.模板需要进行实例化,编译器必须能够访问定义模板的源代码。当调用函数模板或类模板的成员函数时,编译器需要函数定义,需要那些通常放在原文件中的代码。

3. 包含编译模型:
保持头文件和实现文件的分离,在.h文件中进行#include ".cpp"包含,举例:
// A.h
template <typename T>
int compare(const T &, const T&);
#include "A.cpp"

// A.cpp
template <typename T>
int compare(const T &, const T&)
{
}
注意:旧式编译器对每个文件中的模板实例化超过一次,产生多个实例,编译器选择一个实例而抛弃其他的,导致编译时性能显著降低。

4. 分别编译模型:
export关键字能够指明给定的定义可能会需要在其他文件中产生实例化。在一个程序中,一个模板只能定义为导出一次,export关键字不必在模板声明中出现。对类模板声明export更复杂一些。通常,类声明必须放在头文件中,头文件中的类定义体不应该使用关键字export,如果在头文件中使用了export,则该头文件只能被程序中的一个源文件使用。相反,应该在类的实现文件中使用export:
// A.h
template <typename T>
class Queue {};

// A.cpp
export template <typename T>class Queue;
#include "A.h"
// Queue 成员定义
注意:上述.cpp代码中导出类的成员将自动声明为导出的。也可以将类模板中的个别成员声明为导出的,此时,关键字export不再类模板本身指定,而是只在被导出的特定成员定义上指定。导出成员函数的定义不必再使用成员时可见。任意非导出成员的定义必须像在包含模型中一样对待:定义应放在定义类模板的头文件中。

四、类模板成员:
1.当使用类模板的名字的时候,必须指定模板形参,这一规则有个例外:在类本身的作用域内部,可以使用类模板的非限定名。例如,在Queue类的默认构造函数苏、赋值构造函数、赋值操作符、析构函数中,名字Queue是Queue<T>的缩写表示。实质上,编译器推断,当引用类的名字时,引用的是同一版本:
Queue(const Queue & Q);
等价于:
Queue<T>(constQueue<T> & Q);
但是,编译器不会对类中使用的其他模板的模板形参进行这样的推断。

2.类模板的成员函数本身也是函数模板。像任何其他函数模板一样,需要使用类模板的成员函数产生该成员的实例化。与其他函数模板不同的是,在实例化类模板成员函数的时候,编译器不执行模板实参推断,相反,类模板成员函数的模板形参由调用该函数的对象的类型确定,例如,调用Queue<int>类型对象的push成员函数时,实例化的push函数为:
void Queue<int>::push(const int& val);
对象的模板实参能够确定成员函数模板形参。这一事实意味着,调用类模板成员函数比调用类似函数模板更灵活。用模板形参定义的函数形参的实参允许进行常规转换。

3.类模板的成员函数只有为程序所用才进行实例化。如果某函数从未使用,则不会实例化该成员函数。这一行为意味着,用于实例化模板的类型只需要满足实际使用的操作的要求。

4.定义模板类型的对象时,该定义导致实例化类模板,定义对象也会实例化用于初始化该对象的任一构造函数,以及该构造函数调用的任意成员。

5. 非类型形参的模板实参必须是编译时常量表达式。

6. 类模板中的友元声明:
(1)普通非模板类或非模板函数友元:
template <class T>
class Bar
{
    friend classFooBar;
    friend void fun();
};
语义:FooBar的成员和fun函数可以访问Bar类的任意实例的private成员和protected成员。
(2)类模板或函数模板友元:
template <class Type>
class Bar
{
    template<class T> friend class Foo;
    template<class T> friend void fun(const T&);
};
语义:这些友元声明使用与类本身不同的类型参数,Foo的任意实例都可以访问Bar的任意实例的私有元素,类似的,fun的任意实例可以访问Bar的任意实例。
(3)只授予对类模板或函数模板的特定实例的访问权的友元声明:
template <class T> classFoo;
template <class T> voidfun(const T &);
template <class T>
class Bar
{
    friend classFoo<char *>;
    friend voidfun<char *>(char * const&);
};
语义:形参类型为char *的Foo和fun的特定实例可以访问Bar的每个实例。

7.实质上,编译器将友元声明也当做类或函数的声明对待,但是,要想限制对特定实例化的友元关系时,必须在可以用于友元声明之前声明类或函数:
template <class T> classA;
template <class T>
class B
{
    friend classA<T>; // ok,且建立一对一映射
    friend classE<T>; // error
    friend classF<int>; // error
};
如果没有事先告诉编译器该友元是一个模板,则编译器将认为该友元是一个普通非模板类或模板函数。

8. 成员模板:
template <class Type>
class Queue
{
    template<class T> void fun(const T&); // 成员模板
};
在类外部定义成员模板:
template <class Type> template<class T>
void Queue<Type>::fun(const T& val)
{
}
成员模板遵循常规的public、protected、private访问控制。
与其他成员一样,成员模板只有在程序中使用时才实例化。成员模板有两种模板形参:由类定义的和有成员模板定义的。类模板形参由调用函数的对象的类型确定,成员定义的模板形参的行为与普通函数模板一样,这些形参都通过常规模板实参推断而确定。

9. 类模板可以像其他任意类一样声明static成员。
template <class Type>
class Foo
{
public:
    static std::size_tctr;
};
每个实例化表示截然不同的类型,所以给定实例化的所有对象都共享一个static成员。因此,Foo<int>类型的任意对象共享同一个static成员ctr;Foo<string>类型的对象共享另一个不同的ctr成员。

10. 静态成员的访问:
Foo<int> f;
size_t tmp = f.ctr;
size_t st = Foo<int>::ctr;
定义方式像其他任意static数据成员一样,必须在类外部出现数据成员的定义,例如:
template <class Type>
size_t Foo<Type>::ctr = 0;

五、一个泛型句柄类:
1.Handle类的行为类似于指针:复制Handle对象将不会复制基础对象(即Handle内部的成员指针指向对象),复制之后,两个Handle对象将引用同一基础对象。要创建Handle对象,用户需要传递属于由Handle管理的类型(或从该类型派生的类型)的动态分配对象的地址,从此刻起,Handle将“拥有”这个对象。而且,一旦不再有任意Handle对象与该对象关联,Handle类将负责删除该对象。

2. 泛型Handle类的实现 &&使用句柄类:
// Handle.h
#include <iostream>

template <class T>
class Handle
{
public:
    Handle(T * p =NULL);
    Handle(const Handle& h);
    Handle &operator=(const Handle & rhs);
    ~Handle();
    T &operator*();
    T *operator->();
    const T& operator*() const;
    const T *operator->() const;
protected:
private:
    T * ptr;
    size_t * use;
    void rem_ref();
};

template <class T>
Handle<T>::Handle(T * p ) :ptr(p), use(new size_t(1))
{
    
}

template <class T>
Handle<T>::Handle(const Handle& h) : ptr(h.ptr), use(h.use)
{
    ++*use;
}

template <class T>
Handle<T>::~Handle()
{
    rem_ref();
}

template <class T>
void Handle<T>::rem_ref()
{
    if (--*use == 0)
    {
       delete ptr;
       delete use;
    }
}

template <class T>
Handle<T> &Handle<T>::operator=(const Handle& rhs)
{
    ++*rhs.use;
    rem_ref();
    ptr = rhs.ptr;
    use = rhs.use;

    return *this;
}

template <class T>
T &Handle<T>::operator*()
{
    if (ptr)
    {
       return *ptr;
   
    else
    {
       throw std::runtime_error("dereference of unboundHandle");
    }
}

template <class T>
T *Handle<T>::operator->()
{
    if (ptr)
    {
       return ptr;
   
    else
    {
       throw std::runtime_error("access through unboundHandle");
    }
}

template <class T>
const T &Handle<T>::operator *() const
{
    if (ptr)
    {
       return *ptr;
   
    else
    {
       throw std::runtime_error("dereference of unboundHandle");
    }
}

template <class T>
const T *Handle<T>::operator->()const
{
    if (ptr)
    {
       return ptr;
   
    else
    {
       throw std::runtime_error("access through unboundHandle");
    }
}

// test.cpp
#include "Handle.h"

int main()
{
   Handle<int> hp(new int(42));
    if (true)
    {
       Handle<int> hp2 =hp;
       std::cout << *hp<< " "<< *hp2<< std::endl;
       *hp2 = 10;
    }
    std::cout<< *hp<< std::endl;

    return 0;
}

六、模板特化:
1. 模板特化解决的问题是:某些情况下,通用模板定义对于某个类型可能是完全错误的,如下模板对于char*是完全没有意义的:
template <class T>
int compare(const T & v1, const T& v2)
{
    if (v1 <v2) return -1;
    if (v2 <v1) return 1;
    return 0;
}

2. 定义当模板形参类型绑定到const char *时,compare函数的特化:
template <>
int compare<const char *>(constchar * const v1, 
                      const char * const v2)
{
    return strcmp(v1,v2);
}

3. 模板实例化需要注意的一点:
template <class T>
void fun(const T r)
{
}
则:fun<char *>()参数中的r将是char *const类型。
fun<const char *>()参数中的r将是constchar * const类型。
template <class T>
void fun(const T & r)
{
}
则:fun<char *>()参数中的r将是char*的const引用。
fun<const char *>()参数中的r将是constchar *的const引用。

4. 根据2中的模板特化,当调用compare函数时,传递给它两个常量字符指针const char*,编译器将调用特化版本。编译器将为任意其他实参类型(包括普通char *)调用泛型版本:
const char * cp1 = "world", * cp2 = "hi";
int i1, i2;
compare(cp1, cp2); // 调用特化版本
compare(i1, i2); // 调用泛型版本

5. 声明模板特化:
template <>
int compare<const char *>(constchar * const &, 
                      const char * const &);
或者如果可从函数形参表推断模板实参,则不必显式指定模板实参:
template <>
int compare(const char * const&, 
           const char* const &);

6. 在特化中省略空的模板形参表template<>将导致变成重载非模板版本:
int compare(const char * const &, const char *const &);
以上声明了一个普通函数,该函数含有返回类型和可与模板实例化匹配的形参表。

7. 当定义非模板函数的时候,对实参应用常规转换;当特化模板的时候,对实参类型不应用转换。

8. 不是总能检测到重复定义:
如果程序由多个文件构成,模板特化的声明必须在使用该特化的每个文件中出现。不能在一些文件中从泛型模板定义实例化一个函数模板,而在其他文件中为同一模板实参集合特化该函数。

9. 与其他函数声明一样,应在一个头文件中包含模板特化的声明,然后使用该特化的每个源文件中包含该头文件。

10. 普通作用域规则适用于特化:
template <class T>
int compare(const T & t1, const T& t2) {}

int main()
{
    int i = compare("hello","world");
}

template <>
int compare<const char *>(constchar * const & s1, 
                      const char * const & s2)
{
}
特化出现在对该模板实例的调用之后是错误的。对具有同一模板实参集的同一模板,程序不能既有显式特化又有实例化。在声明特化之前,进行了可以与特化相匹配的一个调用,当编译器看到一个函数调用时,它必须知道这个版本需要特化,否则,编译器将可能从模板定义实例化该函数。

11. 类模板特化:
特化可以定义与模板本身完全不同的成员。如果一个特化无法从模板定义某个成员,该特化类型的对象就不能使用该成员。类模板成员的定义不会用于创建显式特化成员的定义。类模板特化应该与它所特化的模板定义相同的接口,否则当用户试图使用未定义的成员时会感到奇怪。

12. 在类特化外部定义成员时,成员之前不能加template<>标记:
void Queue<const char*>::push(const char * val) {}

13. 类模板的部分特化:
如果类模板有一个以上的模板形参,也许想要特化某些模板形参而非全部,可以使用类模板的部分特化:
template <class T1, classT2>
class some_template
{
}
// 部分特化:
template <class T1>
class some_template<T1,int>
{
}
类模板的部分特化本身也是模板。

14. 使用类模板的部分特化:
some_template<int, string> foo;// 使用模板
some_template<string, int> bar;// 使用部分特化
部分特化的定义与通用模板的定义完全不会冲突。部分特化可以具有与通用类模板完全不同的成员集合。类模板成员的通用定义永远不会用来实例化类模板部分特化的成员。

七、重载与函数模板:
1.函数模板可以重载:可以定义有相同名字但形参数目或类型不同的多个函数模板,也可以定义与函数模板有相同名字的普通非模板函数。

2. 声明一组重载函数模板不保障可以成功调用它们,重载的函数模板可能会导致二义性。

3.设计既包含函数模板又包含非模板函数的重载函数集合是困难的,因为可能会使函数的用户感到奇怪,定义函数模板特化几乎总是比使用非模板版本更好。
0 0
原创粉丝点击