C++Primer 16章学习笔记

终于到16章了。。。

16. 模板与泛型编程

16.1 定义模板

16.1.1 函数模板
template <typename t>
int compare(const T& v1, const T& v2){
   if(v1 < v2) return -1;
   else return 1;
 }

模板以定义关键字template开始,后跟一个模板参数列表,用小于号和大于号包围起来。
当使用模板时,我们隐式或显示的指定模板实参,将其绑定到模板参数上。

cout<<compare(1.0)<<endl;

模板类型参数,一般来说可以将模板类型参数看做类型说明符,就像内置类型或内置说明符一样使用,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用作变量声明或类型转换。

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

还可以定义非类型参数,一个非类型参数表示一个值而非一个类型。当一个模板被实例化时,非类型参数被一个值所代替,这个值必须为常量表达式,

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

inline 和 constexptr函数放在模板参数列表之后。
模板的头文件既包括生命也包括定义。

16.1.2 类模板

当我们使用一个类模板时,必须提供显示模板实参,他们被绑定到模板参数。

16.1.3 模板参数

模板参数遵循普通的作用域规则,一个模板参数名的可用范围是在其声明之后,至模板声明或者定义结束前

使用模板的类成员

当编译器遇到如下形式的语句时

T::size_type *p;

它需要知道我们是正在定义一个名为p的变量还是将一个名为size_type的static数据成员相乘。默认情况下,C++语言假定通过作用域访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。我们通过关键字typename 来实现这一点

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

默认模板实参

// compare 有一个默认模板类实参less<T> 和一个默认函数实参F()
template <typename T, typename F = less<T>>
int compare(const T& v1, const T& v2, F f = F()){
   if(f(v1, v2))  return -1;
   if(f(v2, v1))  return 1;
   return 0;
   }

模板默认实参与类模板
无论何时使用一个类模板,我们都必须在模板名之后加上尖括号。尖括号指出类必须从一个模板实例化而来,特别是,如果一个类模板为其所有的模板参数都提供了默认实参,且我们希望使用这些默认实参,则必须在模板名后跟一个括号

template<class T = int> class Numbers{
   public:
      Numbers(T v = 0): val(v) { }
      //对数值的各种操作
   private:
      T val;
  };
 Numbers<long double> lots_of_prediction;
 Numbers<> average_prediction; //空表示希望使用默认类型

16.1.4 成员模板

一个类可以包含本身是模板的函数。这种成员函数被称为成员模板。成员模板不能是虚函数

// 函数对象类,对给定指针执行delete
class DebugDelete{    
   public:       
      DebugDelete(ostream &s = cerr):os(s){};
      // 与任何函数模板类相同,T的类型由编译器判断       
      template <typename T> void operator()(T* p) const{           
      os<< "delete unique_ptr"<<endl;           
      delete(p);       
      }    
   private:      
      ostream &os;
  }

16.1.5 控制实例化

在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显式实例化(explicit instantiation)来避免这种开销,一个显式实例化有如下形式:

extern template declaration; //实例化声明
template declaration;  //实例化类型

declaration 是一个类或函数声明,其中所有参数都已被替换为模板实参

// 实例化声明和定义
extern template class Blob<string>; //声明
template int compare(const int&, const int&); //定义

当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义

由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前。

//Application.cc
//这些模板参数必须在程序的其他位置进行实例化
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2; //实例化会出现其他位置
//Blob<int>及其接受initializer_list的构造函数在本文件中实例化
Blob<int>a1 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
Blob<int>a2(a1);  //拷贝构造函数在本文件中实例化。
int i = compare(a1[0], a2[0]); //实例化出现在其它位置

文件Application.o将包含Blob< int>的实例及其接受的Initializer_list参数的构造函数和拷贝构造函数的实例。而compare< int>函数和Blob< string>类将不在本文件中进行实例化。这些模板的定义必须出现在程序的其他文件中。

//templateBuild.cc
//实例化文件必须为每个在其它文件中声明为extern的类型和函数提供一个(非extern)的定义
template int compare(const int&, const int&);
template class Blob<string>;

16.1.6效率与灵活性

16.2 模板实参推断

16.2.1 类型转换与模板类型转换。

能在调用中应用于函数模板的包括如下两项。
const 转换: 可以将一个非const引用(或指针)传递给一个const的引用(或指针)形参
数组或函数指针转换。如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针

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); // 调用fobj(string, string); const被忽略
fref(s1, s2); // 调用fref(const string&, const string&)
            //s1转化为const是允许的
int a[10],b[42];
fobj(a, b);
fref(a, b); //数组类型不匹配

16.2.2 函数模板显式实参

指定显式模板实参

对于如下模板

template<typename T1, typename T2, typename T3> T1 sum(T2, t3); 

没有任何函数实参的类型可以推断T1的类型,每次调用sum时调用者必须为T1提供一个显式模板实参。

// T1是显式指定的,T2 T3是从函数实参类型推断而来的
auto val3 = sum<long long>(i, ing); // long long sum(int, long)

显式模板实参按从左至右的顺序与对应的模板参数匹配;第一个模板实参与第一个模板参数匹配。第二个实参与第二个参数匹配。依此类推。只有尾部参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。如果我们如此编写

// 糟糕的设计: 用户必须指定所有三个模板参数
template <typename T1, typename T2, typename T3>T3 alternative_sum(T2, T1);

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

template <typename It> auto cn(It beg, It end)->decltype(*beg)
{
   return *beg; //返回序列中一个元素的引用
   }

进行类型转换的标准库模板
有时我们无法直接获得所需要的类型。例如,我们可能希望编写一个类似fcn的函数,但返回一个元素值而非引用。

remove_reference<decltype(*beg)>::type

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

template<typename It>
auto fcn2(It beg, It end)->
   typename remove_reference<decltype(*beg)>::type
   {
      //处理序列
      return *beg;
      }

16.2.4 函数指针和实参推断

假如我们有一个函数指针,它指向的函数返回int, 接受两个参数,每个参数都是指向const int 引用。我们可以使用该指针指向compare的一个实例

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

如果不能从函数的指针类型确定模板实参,则产生错误。

//func 的重载版本,每个版本接受一个不同的函数指针类型
void func(int(*)(const string&, const string*));
void func(int(*)(const int&, const int&));
func(compare) //错误,不知道使用那个实例
func(compare<int>) //传递compare(const int&, const int&)

16.2.5 模板实参推断和引用

考虑如下的例子

template<typename T> void f(T &p);

其中p是一个模板类型参数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);  //错误:传递给一个&参数的实参必须是一个左值
template<typename T> void f2(const T&); //可以接受一个右值
// f2中的参数是const& 实参中的const 是无关的
//在每个调用中,f2的实参都被推断为 const int&
f2(i); // i是一个int; 模板参数T为int
f2(ci); //ci是一个const int, 但模板参数T为int
f2(5); //一个const & 参数可以绑定到一个右值;T是int

引用折叠和右值引用参数
当我们将一个左值(如i)传递给函数的右值引用参数,且此右值一引用指向模板类型参数(如&&)时,编译器推断模板类型为实参的左值引用类型。 因此当我们调用f3(i)时, 编译器推断T的类型为Int&, 而非int
如果我们间接创建一个引用的引用,则这些引用形成了折叠,在所有情况下, 引用会折叠成一个普通的左值引用类型。在新标准中,折叠引用则扩展到右值引用。只在一种情况下引用会折叠成右值引用。右值引用的右值引用。级对于一个X
X& &, X& && 和X&& & 都折叠成类型X&
类型X&& &&都折叠成X&&

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

这两个规则导致了两个重要结果:
如果一个函数参数是一个指向模板类的右值引用(如, T&&), 则它可以被绑定到一个左值;
如果实参是一个左值,则推断出的模板实参类将是一个左值引用,且函数参数将被实例化为一个普通的左值引用。

使用右值引用的函数模板通常使用如下方式进行重载

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

16.2.6理解std::move

标准库的move定义:

//这里的typename 表示类型
template <typename T> typename remove_reference<T>::type&& move(T&& t){
     //static_cast 略去静态
     return static_cast<typename remove_reference<T>::type&&>(t);
     }

对static有一个特殊规则,可以用static_cast显式地将一个左值转换为一个右值引用。当t传入时,T将自动推断其类型,并转换为一个右值引用。

16.2.7 转发

某些函数需要将其一个或多个实参联通类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质。

// 接受一个可调用的对象和另外两个参数的模板
// 对"翻转"的参数调用给定的可调用对象
// flip1是一个不完整的实现;顶层const和引用丢失了
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2){
 f(t2, t1)
 }

当我们希望其调用一个接受引用的函数时就会出现问题

void f(int v1, int& v2){
   cout<<v1<<" "<< ++v2 <<endl;
 }

由于T1在被传递的过程中变成了一个非引用的类型Int, 导致了f中的修改不会影响到实参

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

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

在调用中使用std::forward保持类型信息
我们可以使用一个名为forward的新标准库设施来传递flip2的参数,它能保持原始形参的类型。定义在头文件utility中,forward必须通过显式模板实参来调用。

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

16.3重载与模板

注意
可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可以用函数模板调用的类型转换是非常有限的
如果恰有一个函数提供最优的匹配,则选择此函数。如果有多个同样好的匹配。
如果同样好的函数中只有一个是非模板函数,则选择此函数
如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其它模板更特例化,则选择此模板。
否则此调用有歧义

接下来用一个debug_rep函数来说明

// 打印任何我们不能处理的类型
template <typename T>string debug_rep(const T& t){
  ostringstream ret;
  ret << t; // 使用T的输出运算符打印t的一个表示形式
  return ret.str(); //返回ret绑定的string的一个副本
  }

定义打印指针的debug_rep副本

template<typename T> string debug_rep(T* p){
   ostringstream ret;
   ret << "pointer: "<<p;  // 打印指针本身的值
   if(p)
      ret << " " << debug_rep(*p); // 打印p指向的值
   else
      ret <<" null pointer "; // 或指出p为空
    return ret.str();
    }

此版本生成一个string, 包含指针本身的值和调用debug_rep获得的指针向量的值。此函数不能打印字符指针,因为IO库为char* 定义了一个<<版本。此<<版本假定指针表示一个空字符结尾的字符数组,并打印数组的内容而非地址值。

考虑如下的调用

const string *sp = &s;
cout<<debug_rep(sp)<<endl;

此例中两个模板都是可行的

debug_rep(const string*&); //由第一个版本的debug_rep实例化而来,T被绑定到string*。
debug_rep(const string*), //由第二个版本的debug_rep实例化而来,T被绑定到const string.

根据重载函数模板的特殊规则,此调用被解析为debug_rep(T*), 即,更特例化的版本。
设计这条规则的原因,没有它,无法对一个const的指针调用指针版本的debug_rep。问题在于模板debug_rap(T)本质上可以用于任何类型,包括指针类型。此模板比debug_rep(T)更为通用。没有这条规则,传递const的指针调永远有歧义。

如果使用了一个忘记声明的函数,代码将编译失败,但对于重载函数模板的函数而言,则不是这样。编译器可以从模板实例化出与调用参数匹配的版本,则缺少的声明就不重要了。

16.4可变参数模板

一个可变参数模板就是一个接收可变数目参数的函数或模板类。可变数目的参数被称为参数包。存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包,表示零个或多个模板参数

// Args 是一个模板参数包; reset是一个函数参数包
// Args 表示零个或多个模板类参数
// reset表示零个或多个函数参数
template <typename T, typename...Args>
void foo(const T& t, const Args&... rest);

声明了foo是一个可变模板参数。它有一个名为T的类型参数,和一个名为Args的模板参数包。这个包表示零个或多个额外的类型参数。foo 的函数参数列表包含一个const &类型的函数,指向T的类型,还包含一个名为rest的函数参数包
此包表示零个或多个函数参数。

与往常一样,编译器从函数的实参推断出模板参数类型。对于一个可变模板参数,编译器还会推断包中参数的数目。例如,给定下面的调用:

int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d); //包中含有3个参数

sizeof…运算符可以推断出包中有多少元素。

template<typename ...Args> void g(Args ...args){
    cout<< sizeof...(Args)<<endl;  //类型参数的数目
    cout<< sizeof...(args)<<endl;  // 函数参数的数目
包扩展

print函数的包扩展仅仅将包扩展为其构成元素, C++语言还允许更复杂的扩展模式。例如,我们可以编写第二个可变参数函数,对其每个实参调用debug_rep,然后用print打印string

template <typename... Args>
ostream& errorMsg(ostream &os, const Args&... rest){
   return print(os, debug_rep(rest)...);
  }

然而如下会失败

print(os, debug_rep(rest...));
//上一段代码等价于
print(cerr, debug_rep(fcnName, code.num(), otherData, item));

16.5 模板特例化

在某些时候,通用模板的定义对特定类型是不适合的:通用定义可能编译失败或者做的不正确。
例如:compare

//第一个版本可以比较任意两个类型
template<typename T> int compare(const T&, const T&);
//第二个版本处理字符串字面常量。
template<size_t N, size_t M> 
int compare(const char (&)[N], const char (&)[M]);

只有当我们传递给compare一个字符串常量或者数组时,编译器才会调用接受两个非类型模板参数的版本。如果传递给字符指针,就会调用第一个版本

const char *p1 = "hi", *p2 = "mom";
compare(p1, p2);
compare("hi", "mom");

当我们特例化一个函数模板时,必须为原函数模板中的每个模板参数都提供实参。

// compare的特殊版本,处理字符数组的指针
template<>
int compare(const char* const &p1, const char* const &p2){
   return strcmp(p1, p2);
   }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值