C++ Primer 5th 第16章 模板与泛型编程

模板是C++中泛型编程的基础,一个模板就是创建一个类或者函数的蓝图或者说公式。

C++模板分为函数模板和类模板。C++根据调用模板时传入的具体类型来生成相应类型的具体函数或者类。

类模板则可以是整个类是个模板,类的某个成员函数是个模板,以及类本身和成员函数分别是不同的模板。

 

1.函数模板

函数模板以关键字template开始,后接尖括号括起来的模板参数列表,模板参数列表不允许是空的,也即模板参数至少有一个或多个,多个之间使用逗号分割。

模板参数表示的是函数中用到的类型或者是一个值。当我们使用模板时,根据提供的实参推断出实参的类型,该类型即被用于绑定到模板参数,这个过程被叫做模板的实例化,相应的,生成的版本叫做模板的实例。

模板参数表示的是类型,该类型可以用于制定函数的返回类型或者函数的参数类型,也可以用于函数体内变量声明定义等。

对于每个模板参数列表中的每个类型参数,其前面必须加上关键字typename或class。关键字typename和class之间没有区别。

 

除了定义模板的类型参数,还可以定义一种非类型的参数。非类型参数不表示一种类型,而是表示一个值。非类型参数也不使用关键字typename和class,而是使用具体的类型来指定。

非类型参数在模板实例化时,被用户或者编译器推断出的值所代替,该值必须是常量表达式。(常量表达式是指值不会改变,并且编译期间就能计算得出结果的表达式,字面值属于常量表达式,常量表达式初始化的const对象也是常量表达式)。

例如:

template <unsigned N, unsigned M>
int fun(const char (&p1)[N], const char (&p2)[M])
{
    return strcmp(p1, p2);
}

这里的N,M将会被我们调用fun时传入的实参的值替代:

fun("hi", "mom");

编译器会使用字面常量的大小来替代N,M,并在字符串字面值末尾插入一个空字符作为终止标记,最终实例化为:

int fun(const char (&p1)[3], const char (&p2)[4]);

 

非类型参数可以是整形,也可以是指针或者引用。条件是整形是常量表达式,而指针和引用必须是关联static对象。

 

模板可以声明为inline和constexpr的。

 

当编译器对代码进行编译时,在源码模板定义部分,编译器并没有实际去生成相应的模板代码,只有在使用模板实例化一个具体的版本时,编译器才生成相应的代码。

 

2.类模板

 与函数模板不同,类模板不能为其模板类型参数进行推断。为什么不能为类模板推断类型参数?因为定义类对象的时候,有可能无法提供足够的类型来让编译器进行推断,比如 vector v;这里仅仅定义了一个vector对象,没有提供任何额外的信息让编译器来推断模板参数,所以类模板不为其模板类型参数进行推断,必须我们在实例化时显式指明。

 

类模板的名字不是一个类,也即不是自定义的一个class,而是一个生成class的说明模板。因此使用模板生成的类,必定是带模板类型实参的class,所以一个实例化的class必然有<>来指明类型参数。

 

类模板的成员函数可以在内部定义,这样是隐式内联。也可以在外部定义。类模板的成员函数是一个普通的成员,而不是模板。虽然在外部定义时,成员函数需要以关键字template开始,并且后接模板参数列表,但这不表示该成员函数是个模板。之所以需要关键字template和模板参数列表,是因为成员函数所属的类在实例化时,会具体绑定到一个特定的类型上,成员函数也需要相应被动地绑定到该类型。

 

类模板的成员函数只有被用到的时候才会进行实例化,如果没有被用到,就不会实例化,如同类中定义的成员函数一样,如果不会被用到,那么可以只声明而不定义它。

 

当我们在类模板作用域内进行模板成员的定义时,可以省略模板实参。

 

template <typename T>
class BlobPtr
{
public:
    BlobPtr& operator++();        //无需说明具体类名BlobPtr<T>
    BlobPtr& operator--();        //无需说明具体类名BlobPtr<T>
    BlobPtr  operator--(int);     //无需说明具体类名BlobPtr<T>

    bool empty()
    {
        return data->empty();
    }

    size_t size();

private:
    std::shared_ptr<std::vector<T>> data;
};

template <typename T>
size_t BlobPtr<T>::size()    //类作用域外,需说明具体类名BlobPtr<T>
{
    return data->size();
}

 

 当我们在类模板的外面定义成员函数时,必须以关键字template开始,后接类模板参数列表。

这里为什么类成员函数size( )前要加模板参数列表<T>?template<typyname T>不已经指明了类型了吗? 

前面已经说过:类模板的成员函数是一个普通的成员,而不是模板。这里的template <typename T>是说明size( ) 所属的BlobPtr是个模板类,并且BlobPtr

本身也不是一个类名,是类模板名,真正的类名是BlobPtr<T>,说白了,其实是在具体指明类作用域。所以在类外定义成员函数时,成员函数的参数列表对模板参数是可以省略的。

因此在类模板外面定义普通非模板成员函数时,首先用template <typename T>说明是一个类模板,然后再具体说明该成员函数是哪个类中的成员。后面会看到,如果模板类的成员函数也是一个模板,那么就需要分别各自说明类的模板和函数的模板。

 

3.类模板和友元

如果一个类模板包含一个非模板友元,则友元可以访问该类模板的所有实例。

如果类和友元都是模板,则类实例可以对友元所有实例授权,也可以只授权给特定实例。

为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。

在C++11中,我们也可以模板类型参数声明成友元。例如:

template <typename Type>
class Bar
{
    friend Type;
    //  ...
};

 

新标准也允许我们为类模板定义一个类型别名:

template <typename T> using twin = pair<T, T>;
twin<string, string> authors;    //authors是一个pair<string, string>

 

模板参数遵循普通的作用域规则。与任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字,需要注意的是,在模板内不能重用模板参数名。例如:

typedef double A;
template <typename A, typename B> void f(A a, B b)
{
    A tmp = a;  //类型A隐藏外部typedef double A
    double B;   //错误,不允许重用模板类型参数
}

 

模板声明必须包含模板参数。与函数参数相同,声明中的模板参数的名字不必与定义中相同。

对于一个给定的模板的声明和定义必须有相同数量和种类的参数。

 

关于typename可以用做模板参数的关键字,也可以用来指示类型还是变量名。具体可以参考这篇文章:http://feihu.me/blog/2014/the-origin-and-usage-of-typename/

 

4.类模板的static成员

与普通类一样,模板类也可以拥有static成员,如下:

template <typename T>
class Foo
{
public:
    static std::size_t count() { return ctr; }  //声明并定义
private:
    static std::size_t ctr;    //声明,尚未定义
};

上面这段代码,Foo是一个类模板,它实例化后的类有一个count的静态成员函数和一个静态ctr数据成员。

类的static数据成员有且只有一个定义,类模板也是如此。因此,我们将需要在类模板外定义ctr数据成员,类模板外定义静态数据成员的格式是template关键字开始,后跟模板参数列表,如下:

template <typename T>
size_t Foo<T>::ctr = 0;

上面代码中类名是带模板参数的,因为实例化后的静态数据成员是具体属于某一个类的,而一个具体类则是带模板实参的,仅有类名则只是一个类模板的名字。

 

在新标准中,我们还可以为函数模板和类模板提供默认模板实参,对于类模板,当使用默认实参时,只需使用空的尖括号<>来表示即可。

 

5.成员模板

一个普通类或者类模板可以包含一个模板的成员函数。这种成员被称为成员模板。成员模板不允许是虚函数。

对于类模板,其类和成员有各自独立的模板参数。

与类模板的普通成员函数不同,成员模板是函数模板。当我们在类模板外面定义实现成员模板时,要同时提供类模板和成员模板的参数列表。其中,类模板的参数列表在前,成员模板的在后。例如:

template <typename T>
class Blob
{
    template <typename It> Blob(It b, It e);
    // ...
};

template <typename T>     //类模板的参数列表
template <typename It>     //成员模板参数列表
Blob<T>::Blob(It b, It e):data(std::make_shared<std::vector<T>>(b, e))
{
    //constructor
}

对于含成员模板的类模板,在实例化时,需要同时提供类模板实参和成员模板实参,对于类模板实参,则是显式提供,对于成员模板的实参,则是自动推断。

 

6.控制实例化

模板只有被用到时,编译器才会根据实参进行实例化实参相应的代码,在不同源文件中提供相同实参实例化同一模板时,将会在不同文件中重复生成相同的实例,每个源文件中都会有一个实例。大系统中,这会导致严重的额外开销。

C++11新标准中,可以使用显式实例化来避免这种额外开销。方法是使用实例化声明和实例化定义。

形如:

extern template declaration;     //实例化声明
template declaration;             //实例化定义

下面是实际的例子
extern template class Blob<string>;              // 声明
template int compare(const int&, const int&);     // 定义

上面的代码中,第一行是声明,第二行是定义。需要注意的是,模板的实例化控制的声明和定义是配套使用的。

当编译器遇到extern模板声明时,编译器就不再实例化,它会去程序的其他地方寻找实例,可以多次声明,但只能一次定义。

由于编译器会在使用模板时自动实例化,因此extern声明必须在任何使用当前模板前面声明。

当编译器遇到一个模板的实例化控制的定义时,编译器将会进行实例化,以生成代码。

类模板的实例化定义会实例化模板的所有成员,而不是用到哪个成员才实例化哪个成员。

 

7.

 

posted @ 2016-10-17 09:44  impluse  阅读(513)  评论(0编辑  收藏  举报