C++ Primer 5th学习笔记15 模板与泛型编程

模板与泛型编程

1 定义模板

1.1 函数模板

  类型参数前必须使用关键字classtypename,示例如下:

template<typename T> T valc (const T&, const T&);
template<class U> U valc (const U&, const U&);
template<typename T, class U> T valc (const T&, const U&);

非类型模板参数
  一个非类型参数表示一个值而非一个类型,当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替,这些值必须是常量表达式。示例如下:

template<unsigned N, unsigned M>
int copmare(const char (&p1)[N], const char (&p2)[M])
{
    return strcmp(p1, p2);
}
//当调用如下版本额compare时
compare("hi", "mom");
//编译器会实例出如下版本:
int compare(const char (&p1)[3], const char (&p2)[4]);
//这里编译器会在字符串字面常量的末尾插入一个空字符作为终结符

注意:一个非类型参数可以是一个整型,一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式;绑定到正在或引用非类型参数的实参必须具有静态生存期

inlineconstexpr的函数模板
  函数模板也可以声明为inlineconstexpr,这两个说明符放在模板参数列表之后。示例如下:

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

编写泛型代码时的两个重要原则:

  • 模板中的函数参数是const的引用
  • 函数体中的条件判断仅使用<比较运算

模板编译
  当调用一个函数时,编译器只需要掌握函数的声明。模板则不同:为了生存一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。函数模板和类模板成员函数的定义通常放在头文件中

1.2 类模板

定义类模板
  与函数模板不同,编译器不能为类模板推断模板参数类型,类模板以关键字template,后跟模板参数列表,示例如下:

template<typename T> class Blob
{
public:
    typedef T value_type;
    Blob();
    Blob(std::initializer_list<T> il);
    void push_back(const T &t);
private:
    std::shared_ptr<std::vector<T>> data;
};

类型模板别名
  在C++11中允许为类模板定义一个类型别名:

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

类模板的static成员
  与任何其他类相同,类模板可以声明static成员:

template <typename T> class Foo{
public:
    static std::size_t count(){ return ctr; }
private:
    static std::size_t ctr;
    //所有Foo<X>类型的对象共享相同的ctr对象和count函数
}

与任何其他static数据成员相同,模板类的每个static数据成员必须有且仅有一个定义

1.3 模板参数

使用类的类型成员
  默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。但可以显式告诉编译器该名字是一个类型,通过使用关键字typename来实现这一点:

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

当希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class

1.4 成员模板

成员模板:一个类可以包含本身是模板的成员函数。但成员模板不能是虚函数。
类模板的成员模板
  对于类模板,可以为其定义成员模板。在此情况下,类和成员各自有自己的、独立的模板参数,示例如下:

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

此构造函数有自己的模板类型参数It,作为其两个函数参数的类型

当在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表:

template <template T>    //类的类型参数
template <template It>    //构造函数的类型参数
    Blob<T>::Blob(It b, It e):
        data(std::make_shared<std::vector<T>>(b, e))  {}

1.5 控制实例化

可以通过显式实例化来避免模板在多个文件中实例化相同模板时带来的额外开销。其形式如下:

extern template declaration;    //实例化声明
template declaration;    //实例化定义
//declaration是一个类或函数声明,其中使用模板参数已被替换为模板参数,例如:
//实例化声明与定义
extern template class Blob<string>;    //声明
template int compare(const int&, const int&);    //定义

将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。在一个类模板的实例化定义中,所有类型必须能用于模板的所有成员函数

2 模板实参推断

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

顶层const无论是在形参中还是在实参中,都会被忽略。在其他类型中应用于函数模板的包括如下两项:

  • const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参
  • 数组会函数指针转换:数组实参可以转换为一个指向其首元素的指针;函数实参可以转换为一个该函数类型的指针
    将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换以及数组或函数到指针的转换

2.2 函数模板显式实参

指定显式模板参数
  显式模板实参按由左至右的顺序与对应的模板参数匹配;提供显式模板实参的方式与定义类模板实例的方式相同,显式模板实参在尖括号中给出位于函数名之后,实参列表之前,示例如下:

template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
//调用如下:
//T1是显式指定的 T2和T3是从函数实参类型推断而来的
auto val = sum<long long>(i, lng);    //long long sum(int, long)

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

  当希望确定返回类型时,用显式模板实参表示模板函数的返回类型是很有效的。使用尾置返回类型,来返回一个对象,示例如下:

//尾置返回允许在参数列表之后声明返回类型
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
    //处理序列
    return *beg;    //返回序列中一个元素的引用
}

进行类型转换的标准库模板类
  为了获得元素类型,可以使用标准库的类型转换模板,这些模板定义在头文件type_traits,在以下例子中使用remove_reference来获得元素类型。remove_reference模板有一个模板类型参数和一个名为type的(public)类型成员,若用一个引用类型实例化remove_reference,则type将表示被引用的类型。示例如下:

remove_reference<decltype(*beg)>::type

将获得beg引用的元素的类型:decltype(*beg)返回元素类型的引用类型;remove_reference::type脱去引用,剩下元素类型本身。
组合使用remove_reference、尾置返回以及decltype,就可以在函数中返回元素值的拷贝,示例如下:

//为了使用模板参数成员,必须使用typename
template <typename It>
auto fcn2(It beg, It end) ->
    typename remove_reference<decltype(*beg)>::type
{
    //处理程序
    return *beg;    //返回序列中一个元素的拷贝
}

注意这里type是一个类的成员,而该类依赖于一个模板参数,因此必须在返回类型的声明中使用typename来告知编译器,type表示一个类型,

标准类型转换模板
Mod<T>,其中Mod为 若T为… \rightarrow Mod<T>::type为…
remove_reference X&X&& \rightarrow X;否则 \rightarrow T
add_const X&const X或函数 \rightarrow X;否则 \rightarrow const T
add_lvalue_reference X& \rightarrow TX&& \rightarrow X&;否则 \rightarrow T&
add_rvalue_reference X&或X&& \rightarrow T;否则 \rightarrow T&&
remove_pointer X* \rightarrow X;否则 \rightarrow T
add_pointer X&或X&& \rightarrow X*;否则 \rightarrow T*
make_signed unsigned X \rightarrow X;否则 \rightarrow T
make_unsigned 带符号类型 \rightarrow unsigned X;否则 \rightarrow T
remove_extent X[n] \rightarrow X;否则 \rightarrow T
remove_all_extents X[n1][n2]... \rightarrow X;否则 \rightarrow T

上述每个类型转换模板的工作方式与remove_reference类似,每一个模板都有一个名为type的public成员,表示一个类型。

2.4 函数指针和实参推断

  当用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。举个例子,现有一个函数指针,其指向的函数返回int,接受两个参数,每个参数都是指向const int的引用,使用该指针指向compare的一个实例:

template <typename T> int compare(const T&, const T&);
//pf1指向实例int compare(const T&, const T&);
int (*pf1)(const int&, const int&) = compare;

pf1中参数的类型决定了T的模板实参的类型。
注意:当参数是一个函数模板实例的地址时,程序上下文必须满足:每个模板参数能唯一确定其类型或值,示例如下:

//func的重载版本;每个版本接受一个不同的函数指针类型
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
//显示指出实例化哪个compare版本
func(compare<int>);    //传递compare(const int&, const int&)

2.5 模板实参推断和引用

从左值引用函数参数推断类型
  当一个函数参数是模板类型的一个普通(左值)引用时(即,形如T&),绑定规则只能传递给它一个左值。实参可以是const类型,也可以不是,若实参是const的,则T将被推断为const类型,示例如下:

template <typename T> void f1(T&&);    //实参必须是一个左值
//对f1的调用使用实参所引用的类型作为模板参数类型
f1(i);    //i是一个int;模板参数类型T是int
f1(ci);    //ci是一个const int;模板参数T是const int
f1(5);    //错误:传递给一个&参数的实参必须是一个左值

  如果一个函数参数的对象是const T&,正常的绑定规则告诉我们,可以传递给它任何类型的实参——一个对象(const或非const)、一个临时对象或是一个字面常量值。若函数参数本身是const时,T的类型推断的结果不会是一个const类型,具体示例如下:

template <typename T> void f2(const T&);    //可以接受一个右值
//f2中的参数是const &;实参中的const是无关的
f2(i);    //i是一个int;模板参数类型T是int
f2(ci);    //ci是一个const int;但模板参数T是int
f2(5);    //一个const &参数可以绑定到一个右值;T是int

从右值引用函数参数推断类型
  当一个函数参数是一个右值引用(即形如T&&)时,正常绑定规则告诉我们,可以传递给它一个右值。当这样做时,类型推断过程与普通左值引用函数参数的推断类似。推断出的T的类型是该右值实参的类型:

template <typename T> void f3(T&&);
f3(42);    //实参是一个int类型的右值;模板参数T是int

引用折叠和右值引用参数
  使用第二个例外绑定规则:若间接创建一个引用的引用,则这些引用形成了“折叠”,在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,折叠规则扩展到右值引用。只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。即,对于一个给定类型X:

  • X& &X& &&X&& &都会折叠成类型X&
  • 类型X&& &&折叠成X&&
    注意:引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数

引用折叠规则和右值引用的特殊类型推断规则组合在一起,导致了两个重要结果:

  • 如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),这可以被绑定到一个左值,即可以传递给它任意类型的实参
  • 若实参是一个左值,这推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个(普通)左值引用参数(T&

2.6 理解std::move

std::move是如何定义的
标准库定义的move:

//在返回类型和类型转换中也要用到typename
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_cast<typename remove_reference<T>::type&&> (t);
}

首先,move函数参数T&&是一个指向模板类型参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配,既可以传递给move一个左值,也可以传递给它一个右值:

string s1("hi!"), s2
s2 = std::move(string("bye!"));    //正确:从一个右值移动数据 
s2 = std::move(s1);    //正确:但在赋值之后,s1的值是不确定的

std::move是如何工作的
 &emap;在第一个赋值中,传递给move的实参是string的构造函数的右值结果——string("bye!")。因此在std::move(string("bye!"))中:

  • 推断出的T的类型为string
  • 因此,remove_referencestring进行实例化
  • remove_reference<string>的type成员是string
  • move的返回类型是string&&
  • move的函数参数t的类型为string&&

因此,这个调用实例化move<string>,即函数:string&& move(string &&t),函数体返回static_cast<string&&>(t),t的类型已经是string&&

 &emap;在第二个赋值调用了std::move(),在此调用中,传递给move的实参是一个左值。这样:

  • 推断出的T的类型为string&(string的引用,而非普通string)
  • 因此,remove_referencestring&进行实例化
  • remove_reference<string&>的type成员是string
  • move的返回类型是string&&
  • move的函数参数t实例化为string& &&,会折叠为string&

因此,这个调用实例化move<string&>,即函数:string&& move(string &t),函数体返回static_cast<string&&>(t),t的类型为string&,cast将其转换为string&&

从一个左值static_cast到一个右值引用是允许的
 &emap;通常情况下,static_cast只能用于其他合法的类型转换,但是此处有一条针对右值引用的特许规则:虽然不能隐式地将一个左值转换为右值引用,但可以使用static_cast显式地将一个左值转换为一个右值引用

2.7 转发

  某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,需要保持被转发实参的使用性质,包括实参类型是否是const的以及实参是左值还是右值。
定义能保持类型信息的函数参数
  通过将一个函数参数定义为一个指向模板类型参数的右值引用,可以保持其对应实参的所有类型信息;而使用引用参数(无论是左值还是右值)则可以保持const属性,因为在引用类型中的const是底层的。如果将函数参数定义为T1&&T2&&,通过引用折叠就可以保持翻转实参的左值/右值属性,示例如下:

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

注意:如果一个函数参数是指向模板类型参数的右值引用(如T&&),其对应的实参的const属性和左值/右值属性将得到保持。

在调用中使用std::forward保持类型信息
  可以使用一个名为forward的新标准库设施来传递flip2的参数,其定义在头文件utility中,与move不同,forward必须通过显式模板实参来调用。forward返回该显式实参类型的右值引用。即:forward<T>的返回类型是T&&
  通常情况下。使用forward传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性,示例如下:

template <typename Type> intermediary(Type &&arg)
{
    finalFcn(std::forward<Type>(arg));
    // ...
}

这里由于arg是一个模板类型参数的右值引用,Type将表示传递给arg的实参的所有类型信息。如果实参是一个右值,则Type是一个普通(非引用)类型,forward<Type>将返回Type&&;如果实参是一个左值,则通过折叠引用,Type本身是一个左值引用类型。在此情况下,返回类型是一个指向左值引用类型的右值引用,再次对forward<Type>的返回类型进行引用折叠,将返回一个左值引用类型。

使用forward,重写翻转函数:

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

3 可变参数模板

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

3.1 编写可变参数函数模板

  可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。print函数也是这样的模式,每次递归调用将第二个实参打印到第一个实参表示的流中。为了终止递归,还需要调用一个非可变参数的print函数,该函数接受一个流和一个对象:

//用来终止递归并打印最后一个元素的函数
//此函数必须在可变参数版本的print定义之前声明
template<typename T>
ostream &print(ostream &os, const T &t)
{
    return os << t;    //包中最后一个元素之后不打印分隔符
}

//包中除了最后一个元素之外的其他元素都会调用中版本的跑print
template<typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)
{
    return os << t << ", ";    //打印第一个实参
    return print(os, rest...);    //递归调用,打印其他实参
}

调用示例如下:

print(cout, i, s, 42);    //包中有两个参数
//执行流程如下
print(cout, i, s, 42);    //t = i; rest.. = s,42
print(cout, s, 42);    //t = s; rest.. = 42
print(cout, 42);    //调用非可变参数版本的print

注意:当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中。否则可变参数版本会无限递归

3.2 转发参数包

  在新标准下,可以组合使用可变参数模板与forward机制来编写函数,实现将其实参不变地传递给其他参数。如下面一个例子,为StrVec类添加一个emplace_back成员。标准库容器的emplace_back成员是一个可变参数成员模板,它用其实参在容器管理的内存空间中直接构造一个元素。示例如下:

class StrVec{
public:
    template <class... Agrs> void emplace_back(Args&&...);
    //这里模板参数包扩展中的模式是&&,即表示参数是一个指向其对应实参的右值引用
}
template <class... Agrs>
inline
void StrVec::emplace_back(Args&&... args)
{
    chk_n_alloc();    //如果需要的话,重新分配StrVec内存空间
    alloc.construct(first_free++, std::forward<Args>(args)...);
}

construct调用中的扩展为:
std::forward<Args>(args)...
它既扩展了模板参数包Args,也扩展了函数参数包args。此模式生成如下形式的元素:
std::forward< T i T_i >( t i t_i )
其中 T i T_i 表示模板参数包中第 i i 个元素的类型, t i t_i 表示函数参数包中第 i i 个元素

转发和可变参数模板
  可变参数函数通常将其参数转发给其他函数,这种函数通常具体如下通用的形式:

//fun有零个或多个参数,每个参数都是一个模板参数类型的右值引用
template <typename... Agrs>
void fun(Args&&... args)    //将Args扩展为一个右值引用的列表
{
    //work的实参既扩展Args又扩展args
    work(std::forward<Args>(args)...);
}

4 模板特例化

定义函数模板特例化
  当特例化一个函数模板是,必须为原模板中的每个参数都提供实参。使用关键字template后跟一个空尖括号对(<>)。尖括号指出将为原模板的所有参数提供实参,示例如下:

//compare的特殊版本,处理字符数组的指针
template <>
int compare(const char* const &p1, const char* const &p2)
{
    return strcmp(p1, p2);
}

这里T为const char*,函数要求一个指向此类型const版本的引用,这里函数的参数为const char* const &,即一个指向const charconst指针的引用。

函数重载与模板特例化
  特例化的实质是实例化一个模板,而非重载它,因此特例化不影响函数匹配。
  模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前,然后是这些模板的特例化版本。

类模板特例化
  以下面的例子:为标准库hash模板定义一个特例化版本,用它来将Sales_data对象保存在无序容器中。一个特例化hash类必须定义:

  • 一个重载的调用运算符,接受一个容器关键字类型的对象,返回一个size_t。
  • 两个类型成员,result_typeargument_type,分别调用运算符的返回类型和测试类型。
  • 默认构造函数和拷贝赋值运算符
    必须早原模板定义所在的命名空间中特例化它,为了向命名空间添加成员,首先必须打开命名空间,示例代码如下:
//打开std命名空间,以便特例化std::hash
namespace std{
    
}    //关闭std命名空间;注意这里右括号之后没有分号
//花括号对之间的任何定义都将成为命名空间std的一部分

能够处理Sales_data的特例化hash版本,代码示例如下:

//打开std命名空间,以便特例化std::hash
namespace std{
template <>    //特例化版本,模板参数为Sales_data
struct hash<Sales_data>
{
    //用来散列一个无序容器的类型必须要定义一下类型
    typedef size_t result_type;
    typedef Sales_data argument_type;
    size_t operator() (const Sales_data& s) const;
    //类使用合成的拷贝控制成员和默认构造函数
};
size_t
hash<Sales_data>::operator() (const Sales_data& s) const
{
    return hash<string>()(s.bookNo) ^
           hash<unsigned>()(s.units_sold) ^
           hash<double>()(s.revenue);
}
    
}    //关闭std命名空间;注意这里右括号之后没有分号

这里在operator()函数中,使用一个hash<string>()对象来生成bookNo的哈希值,用一个hash<unsigned>()对象来生成units_sold的哈希值,用一个hash<double>()对象来生成revenue的哈希值,再将这些结果进行异或运算,形成给定Sales_data对象的完整的哈希值。

猜你喜欢

转载自blog.csdn.net/qq_18150255/article/details/89605439