《C++ Primer》读书笔记 第16章:模板与泛型编程

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

16.1 定义模板

16.1.1 函数模板

定义

模板定义以关键字template开始,后跟一个模板参数列表。在模板定义中,参数列表不能为空。

template T>
int compare(const T &v1, const T &v2)
{
    ...
}

实例化函数模板

当我们使用一个函数模板时,编译器用函数实参来为我们推断模板实参,然后用模板实参来为我们实例化一个特定版本的函数,编译器生成的函数版本称为该函数模板的实例。

模板类型参数

在函数模板中,通过关键字class或者typename来指定模板类型参数,在函数模板中,类型参数可以用来指定返回值或者函数的参数类型,以及可以用于变量声明或类型转换。

template T>
T foo(T *p)
{
    T tmp = *p;
    return tmp;
}

非类型模板参数

除了定义类型参数,我们还可以在模板中定义非类型参数。一个非类型参数表示一个值而非一个类型。我们通过特定的类型名而非class或typename来指定非类型参数。非类型参数的值必须是常量表达式,以便编译器在编译时实例化模板。

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

inline和constexpr的模板函数

函数模板可以声明为inline或constexpr,inline或constexpr关键字应放在模板参数列表之后,返回值之前:

template<typename T>
inline T min(const T&, const T&)

编写类型无关的代码

模板程序应该尽量减少对实参类型的要求,比较一下两个模板:

template<typename T>
int compare(const T &v1, const T &t2)
{
    if(v1 < v2) return -1;
    if(v1 > v2) return 1;
    return 0;
}

template<typename T>
int compare(const T &v1, const T &t2)
{
    if(v1 < v2) return -1;
    if(v2 < v1) return 1;
    return 0;
}

第一个模板要求模板实参同时支持“<””>”操作符,但是第二个模板的模板实参只需要支持“<”操作符即可,故二个模板的实现优于第一个模板。

模板编译

当编译器遇到一个模板定义时,它并不生成代码,只有当我们实例化出模板的一个特定版本时(即当我们使用时),编译器才生成代码。为了生成一个实例化模板,编译器需要掌握函数模板或者类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常即包含声明也包含定义。

16.1.2 类模板

类模板时用来生成类的蓝图。与函数模板不同,编译器不能为类模板推断模板参数类型,当我们使用类模板时,我们必须额外提供模板参数信息。

定义类模板

形式与定义函数模板类似

templateT>
class Blob
{
    ...
};

实例化类模板

我们需要提供模板参数来实例化类。

Blob<int> ia;
Blob<double> da;

为每一种模板参数类型,编译器都会为其生成一种类类型。

在模板作用域中引用模板类型

一个类模板的代码如果使用了另外一个模板。通常不应该将一个实际类型的名字作为其模板参数,我们通常将模板自己的模板参数当做使用模板的参数:

template<typename> 
class Blob
{
    pritevate:
        std::shared_ptr<std::vector> data;
}

类模板的成员函数

与其他类相同,我们即可以在类模板内部,也可以在类模板外部为其定义成员函数,定义在类模板内部的成员函数被隐式声明为内联函数。
定义在类模板之外的成员函数必须以template开始,后接模板参数列表,而且从一个模板生成的类的名字必须1包含其模板实参。

template<typename T>
ret-type Blob<T>::member-name(parm-list)

类模板成员函数的实例化

默认情况下,对于一个实例化了的类,其成员函数只有当程序用到它时才进行实例化。

在类代码内简化模板类名的使用

在类模板自己的作用域中,我们可以直接使用模板名而不必提供模板实参。

在类模板外使用类模板名

当我们在类模板外定义其成员时,必须记住,我们并不在类的作用域中,直到遇见类名我们才进入类的作用域,特别注意的是,返回值不在类的作用域中。

类模板与友元

如果一个类模板包含了一个非模板友元,则友元被授予可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。

一对一友元关系

类模板与另一模板间友好关系的最常见形式是建立对应实例及其友元间的友好关系。

templateT> class BlobStr;
templateT>
class Blob
{
    friend class BlobStr<T>;
}

通用和特定的友元关系

一个类可以将另一个类的每个模板声明为自己的友元,或者限定特定的实例为友元:

template<typename T> class Pal0;
template<typename T> class Pal1;
template<typename T> class c
{
    friend class Pal0;
    friend class Pal1;
    //声明Pal2的所有实例为友元,这种情况不需要前置声明
    template<typename x> friend class Pal2;
}

令模板自己的类型参数为友元

在C++11x新标准中,我们可以将模板类型参数设定位友元

template class Bar
{
    friend Type;
    ...
}

模板类型别名

在新标准中,我们可以为类模板定义一个类型别名:

template<typename T> using twin = pair;
twin<int> win_loss;
twin<double> area;

twin是一族类的别名,win_loss和area是特定类型的类型别名。

类模板的static成员

与其他任何类型一样,类模板可以声明为static成员。

template<typename T>
class Foo
{
    public:
        static std::size_t count() {return ctr;}
    private:
        static std::size_t ctr;
}
auto ct = Foo<int>::count();

每个Foo的实例都有自己的static成员实例,为了通过类直接访问static成员,我们必须引用一个特定的实例。

16.1.3 模板参数

模板参数与作用域

与任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字,但是,在模板内不能重用模板参数名。

typedef doule A;
template<typename A, typename B>
void f(A a, B b)
{
    A tmp = a;      //A为模板参数类型A
    double B;       //错误,不能与模板参数同名
}

模板声明

模板声明必须包含模板参数。
一个特定文件所需要的所有模板的声明通常一起放置在文件的开始位置,出现于任何使用这些模板的代码之前。

使用类的类型成员

默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。如果我们希望使用一个模板类型参数的类型成员,则需要用关键字typename(不能使用class)来显式指定:

template <typename T>
typename T::value_type top(const T &c)
{
    if (!c.empty())
        return c.back();
    else
        return typename T::value_type();
}

默认模板实参

在新标准中,我们可以为函数和类模板提供默认实参,其形式与不同函数的默认实参类似:

template T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F())
{
    ...
}

无论何时使用一个类模板,我们都必须加上尖括号,即使该类模板为所有类型参数提供了默认值,我们也应该提供一个空的尖括号。

Number<> average_precision;

16.1.4 成员模板

一个类(无论是普通类还是模板类)可以包含本身是模板的成员函数。这种成员被称为成员模板。成员模板不能是虚函数。

类模板的成员模板

类和自己的成员模板各自有自己独立的模板参数。当我们在模板外定义一个成员模板时,必须同时我提供类以及成员函数的模板参数列表,类模板的参数列表在前,后跟成员自己的参数列表:

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

16.1.5 控制实例化

当模板被使用时才实例化,这意味着,相同实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供相同的模板参数时,每个文件都会有该模板的一个实例。
我们可以通过显式实例化来避免这种开销。我们通过关键字extern来进行显式实例化。

extern template class Blob<string>

当编译器遇到extern模板声明时,它不会在本文件中生成实例代码。

实例化定义会实例化所有成员

一个类的实例化定义会实例化该模板的所有成员。即使我们不使用某个成员,他也会被实例化。

template class Blod<string>;

16.2 模板实参推断

16.2.1 类型转换与模板类型参数

模板类型参数通常不会进行类型转换,只有有限的几种类型转换会自动应用于这些实参,如下:

  1. const转换:可以将一个非const对象的引用或指针传递给一个const的引用与指针。
  2. 一个数组实参可以转换为一个指向其首元素的指针,一个函数实参会转换为一个该函数类型的指针。
template<typename T> T fobj(T, T);
template<typename T> T fref(const T&, const T&)
string s1("a value");
const string s2("another value");
fobj(s1, s2);    
fref(s1, s2);

int a[10], b[42]
fobj(a, b);   //正确,调用fobj(int *, int *)
fref(a, b);   //错误,无法进行模板实参推断,const int (&)[10]与const int (&)[42]不是同一种类型
int c = 10; double d = 3.14;
fobj(a, b);   //错误,不会进行内置类型的类型转换,故无法推断模板类型实参。

16.2.2 函数模板显式实参

有时,编译器无法推断出模板实参的类型,比如函数返回类型与参数列表中的任何类型均不相同。

template<typename T1, typename T2, typename T3>
T1 sum(T2 v1, T3 v2)
{
    return T1(v1 + v2);
}

这样我们每次都必须为T1提供一个显式模板实参。

int i = 0;
long lng = 1;
auto val3 = sum<long long>(i, lng);

后面的类型参数可以被推断出,故可以省略。只有尾部参数的显式实参才可以省略,而且前提是它们可以从函数参数中推断出来。

正常类型转换应用于显式指定的形参

如果显式指定了模板类型参数,则可以进行正常的类型转换。

template<typename T> int compare(const T &v1, const T &v2);
compare(3, 3.14);       //错误
compare<int>(3, 3.14);  //正确

16.2.3 尾置返回类型与类型转换

尾置返回类型即将返回值放在函数声明末尾,如下:

template<typename It> 
auto fcn(It beg, It end) ->decltype(*beg)
{
    return *beg;
}

进行类型转换的标准库模板类

在泛型编程中,我们常常需要获得元素类型,这时我们可以使用标准库的类型转换模板,它们定义在头文件type_traits中,这个类为模板元编程设计。常用的有remove_reference、add_const等。

16.2.4 函数指针与实参推断

当我们用一个模板实例来初始化一个函数指针或为一个函数指针赋值,编译器会使用指针的类型来推断模板实参。

template<typename T> int compare(const T &, const T &);
int (*pf1)(const int &, const int &) = compare;

16.2.5 模板实参推断和引用

从左值引用函数参数推断类型

当一个函数参数是模板参数类型的一个普通(左值)引用时,绑定规则告诉我们,只能传递给它一个左值。实参可以是const,也可以不是。

template <typename T> void f1(T &);
int i = 0;
f1(i);      //正确
f1(5);       //错误,传递的实参必须是一个左值

从右值引用函数参数推断类型

当一个函数参数是一个右值引用,正常绑定规则告诉我们必须给它传递一个右值。

template<typename T> void f3(T &&);
f3(43);     //正确,T推断为int

引用折叠和右值引用参数

我们可能认为f3(i)这样的调用时不合法的,毕竟我们不能把一个右值引用绑定到一个左值上,但是c++语言在正常绑定规则之外定义了两个例外规则,允许这种绑定。
第一个规则,当我们将一个左值传递给函数的右值引用,且此右值引用指向函数模板类型参数,则编译器推断模板参数类型为实参的左值引用类型,因此f3(i)中T被推断为int &。
第二个例外规则,如果我们间接创建一个引用的引用,则这些引用形成折叠,折叠规则如下:

  1. X& &, X& &&, X&& &都折叠为X&
  2. X&& && 折叠为X&&

引用折叠只能应用于间接创建的引用的引用,如类型别名或函数模板。
这两个规则暗示,我们可以将任何类型的实参传递给T&&类型的函数参数。

编写接受右值引用参数的模板函数

模板参数可以被推断为一个引用类型,这一特性对模板内的代码可能有令人惊讶的影响:

template
void f3(T &&val)
{
    T t = val;    //拷贝还是绑定一个引用?
    t = fcn(t);   //赋值是只改变t还是即改变t又改变val?
}

这样编写正确的代码将变得异常困难。
实际上右值引用通常用于两种情况:模板转发其实参或模板被重载。
使用右值引用的函数模板通常以如下的形式进行重载:

template<typename T> void f(T &&);   //绑定到非const右值
template<typename T> void f(const T &);    //绑定到左值以及const右值

16.2.6 理解std::move

std::move 是如何定义的

标准库中这样定义std::move

template<typename T>
typename remove_reference::type && move(T &&t)
{
    return static_cast<typename remove_reference::type &&>(t);
}

首先,函数参数为T&&,其可以与任何类型的实参匹配。其次,无论怎样该函数均会返回一个右值引用。
另外,虽然不能隐式的将一个左值转换为一个右值引用,但是我们可以用static_cast实现这一点

16.2.7 转发

某些情况下我们需要将一个或多个实参连同类型不变地转发给其他函数,包括是是否是const以及是左值还是右值。

void f(int v1, int &v2)
{
    ...
}

templateF, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
    f(t1, t2);
}

flip(f, i, j)

在上述代码中,我们调用f改变了v2的值,但是并未改变j的值。

定义能保持类型信息的函数参数

通过将一个函数参数定义为一个指向函数类型参数的右值引用,我们可以保持其对应实参的所有类型信息。

template<typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 && t2)
{
     f(t1, t2);
}

但上面版本的flip仍有缺陷,即不能用于接受右值引用参数的函数:

void g(int &&i, int &j)
{
    ...
}
flip(g, i, 42) //错误, 不能从一个左值实例化int &&

为此我们可以使用forward的新标准库,他能保持原始参数的类型。

templateF, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
    f(std::forward(t2), std::forward(t1));
}

16.3 重载与模板

函数模板可以被另一个模板或一个普通非模板函数重载。
匹配优先级:非模板函数 > 更特例化的函数模板 > 其他模板。

16.4 可变参数模板

一个可变参数模板就是一个接受可变数目参数的模板函数或模板类,可变数目的参数被称为参数包。存在两种参数包:模板参数包,函数参数包。
class…或typename…指出接下来的参数表示零个或多个类型的列表,一个类型名后接省略号表示多个给定类型的非类型参数的列表。

template T, typename... Args>
void foo(const T &t, const Args& ... rest);

sizeof… 运算符

当我们需要知道包里有多少元素时,可以使用sizeof…运算符。

template<typename ...Args>
void g(Args ...args)
{
    cout << sizeof...(Args);      //类型参数数目
    cout << sizeof...(args);      //函数参数数目
}

16.4.1 编写可变参数函数模板

可变参数函数通常是递归的,查看下面的实例:

template
ostream &print(ostream &os, const T &t)
{
     return os << t;
}

template
ostream &print(ostream &os, const T &t, const Args& ...rest)
{
    os << t << ",";
    return print(os. rest...);
}

16.5 模板特例化

模板的特例化版本就是模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型。

template<>
int compare(const char * const &p1, const char * const &p2)
{
    ...
}

一个特例化版本实质上是一个模板的实例,而非函数名的一个重载版本。
为了特例化一个模板,原模板的声明必须在作用域中。模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。

你可能感兴趣的:(《C++ Primer》读书笔记 第16章:模板与泛型编程)