C++Primer 第十六章模板与泛型编程

第十六章 模板与泛型编程

16.1 定义模板

16.1.1 函数模板

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

模板定义以关键字template开始,后面跟上一个模板参数列表,编译器会根据传递实参的类型来推断模板参数的类型。

模板类型参数

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

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

类型参数可以作为内置类型或类类型说明符来使用,也可以用来指定函数的返回类型。使用类型参数前必须使用关键字classtypename

非类型模板参数

template<unsigned N, unsigned M>
int compare1(const char (&p1)[N], const char (&p2)[M]) {
  return strcmp(p1, p2);
}
std::cout << compare1("hi", "mom") << std::endl;

非类型模板参数的模板实参必须是常量表达式。

函数模板可以声明为inlineconstexpr的,但是必须放在模板参数列表之后。

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

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

模板编译

函数模板何类模板成员函数的定义通常放在头文件中。

16.1.2 类模板

类模板与函数模板不同,编译器无法推断类模板模板参数类型。

定义类模板

template<typename T> class Blob {
public:
  using value_type = T;
  using size_type = std::vector<T>::size_type;
  Blob();
  Blob(std::initializer_list<T> li);
  size_type size() const { return data->size(); }
  bool empty() const { return data->empty(); }
  void push_back(const T &t) { data->push_back(t); }
  //移动版本的push_back
  void push_back(T &&t) { data->push_back(std::move(t)); }
  void pop_back();
  T& back();
  T& operator[] (size_type i);
private:
  std::shared_ptr<std::vector<T>> data;
  void check(size_type i, const std::string &msg) const;
};

实例化类模板。

Blob<int> ia;
Blob<string> names;
Blob<double> prices;

编译器会从Blob模板实例化出一类类,重写Blob模板,将模板参数T的每个实例替换为给定的模板实参。如果我们在类的外部定义成员函数,需要在成员函数之前加上模板参数列表,因为类模板的成员函数具有和模板相同的模板参数。

template<typename T>
void Blob<T>::check(size_type i, const std::string &msg) const {
  if (i >= data->size()) throw std::out_of_range(msg);
}

对应的成员因该是:

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

类模板的成员函数只有在使用的时候才会实例化。

Blob<int> squares = {0,1,2,3,4};  //实力换initializer_list构造函数
 for (size_t i = 0; i != squares.size(); ++ i)
  squares[i] = i * i; //实例化Blob下标运算符

在类模板自己的作用域中,可以直接使用模板名而不提供实参。注意只有遇到类模板名的时候才表示进入类模板作用域。

template<typename T>
class BlobPtr {
public:
  BlobPtr() : curr(0) {}
  BlobPtr(Blob<T> &a, size_t sz = 0) : wptr(a.data), curr(sz) {}
  T& operator*() const {
    auto p = check(curr, "dereference past end");
    return (*p)[curr];
  }
  BlobPtr& operator++();  //前置运算符
  BlobPtr& operator--();
private:
  std::shared_ptr<std::vector<T>> check(std::size_t, const std::string &) const;
  std::weak_ptr<std::vector<T>> wptr;
  std::size_t curr;
};
template <typename T>
BlobPtr<T>& BlobPtr<T>::operator++() {
  BlobPtr ret = *this;  //在类的作用域内,无需重复模板实参
  ++*this;
  return ret; //返回保存的状态
}

一对一友好关系

//Blob友元需要,需要进行前置声明
template<typename T> class BlobPtr;

template<typename T> class Blob;

template<typename T>
bool operator==(const Blob<T>&, const Blob<T>&);

template <typename T>
class Blob {
  friend class BlobPtr<T>;
  friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
};
Blob<char> ca;  //BlobPtr<char>和operator==<char>是本对象的友元

BlobPtr<char>的成员可以访问ca的非public部分,但是caia或Blob的任何其他实例都没有特殊访问权限。

通用和特定的模板友好关系

template <typename T> class Pal;
class C {
  friend class Pal<C>;  //用类C实例化的Pal是C的一个友元
  //Pal2的所有实例都是C的友元,此时无需前置声明
  template <typename T> friend class Pal2;
};

template <typename T> class C2 {
  //C2的每个实例将相同实例化的Pal声明为友元
  friend class Pal<T>;
  //Pal2的每个实例都是C2的每个实例的友元,不需要前置声明
  template <typename X> friend class Pal2;
  //Pal3是一个非模板类,它是C2所有实例的友元
  friend class Pal3;  //不需要Pal3的前置声明
};

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

template <typename Type> class Bar {
  friend Type;  //将访问权限授予用来实例化Bar的类型
};

将用来实例化Bar的类型声明为友元,因此对于某个类型别名FooFoo将成为Bar<Foo>的友元,Sales_data成为Bar<Sales_data>的友元。

模板类型别名

模板不是一个类型,无法定义一个typedef引用Blob<T>。但是我们可以使用using来使用类型别名。

template<typename T> using twin = std::pair<T,T>;

template<typename T> using partNo = std::pair<T,unsigned>;
twin<std::string> authors;  //authors是一个pair<string,string>
twin<int> win_loss;    //win_loss是一个pair<int,int>
twin<double> area;    //area,是一个pair<double,double>
partNo<std::string> books;  //books是一个pair<string,unsigned>

类模板的static成员

对任意给定类型,都有一个Foo<X>::ctrFoo<X>::count成员,所有Foo<X>类型的对象共享相同的ctr对象和count对象。

template<typename T> size_t Foo<T>::ctr = 0;  //定义并初始化ctr
//实例化static成员Foo<string>::cstr, Foo<string>::count
Foo<std::string> fs,fs2,fs3;
//所有三个对象共享相同的Foo<int>::ctr和Foo<int>::count成员
Foo<int> fi, fi2, fi3;
auto ct = Foo<int>::count();  //实例化Foo<int>::count
ct = fi.count();  //使用Foo::count
//ct = Foo::count();  //error,不知道使用哪个版本的count

16.1.3 模板参数

在模板内不能重用模板名.

template <typename A, typename B> void f(A a, B b) {
  A tmp = a;
  double B; //error,重声明模板参数B
}

模板声明和模板定义的参数可以不同。

//三个都是同一个模板
template <typename T> T calc(const T&, const T&);
template <typename U> U calc(const U&, const U&);
template <typename Type> Type calc(const Type&, const Type&);

使用类的类型成员

使用一个模板类型参数的类型成员,必须显式告诉编译器该名字是一个类型,使用关键字typename来实现这一点。

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

默认模板实参

template <typename T, typename F = std::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;
}

F 表示可调用对象类型,并定义了一个新的函数参数f,绑定到一个可调用对象上。和函数默认实参一样,对于一个模板参数,只有当它右侧的所有对象都有默认实参时,它才可以有默认实参。

template <class T = int> class Numbers {
public:
  Numbers(T v = 0) : val(v) {}
private:
  T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average_precision; 

average_precisionint代替T实例化得到,lots_of_precisionlong double代替T实例化得到。

16.1.4 成员模板

类的成员函数是模板,这类成员函数称为模板成员。

普通类(非模板)的成员模板

class DebugDelete {
public:
  DebugDelete(std::ostream &s = std::cerr) : os(s) {}
  template<typename T> void operator() (T *p) const {
    os << "deleting unique_ptr" << std::endl;
    delete p;
  }
private:
  std::ostream &os;
};
double *p = new double;
DebugDelete d;
d(p); //调用DebugDelete::operator释放p
int *ip = new int;
DebugDelete()(ip);
std::unique_ptr<int, DebugDelete> up(new int, DebugDelete());
std::unique_ptr<std::string, DebugDelete> sp(new std::string, DebugDelete());

unique_ptr调用析构函数的时候会对成员模板实例化。

//成员模板实例化
void DebugDelete::operator() (int *p) const { delete p; }

类模板的成员模板

类模板和其成员模板的模板参数不同,并且在类的外部定义成员模板,先加上类模板的模板参数,再加上成员模板的模板参数。

template<typename T>
template<typename Iterator>
Blob<T>::Blob(Iterator b, Iterator e) {
  data(std::make_shared<std::vector<T>>(b, e));
}

16.1.5 控制实例化

如果多个源文件使用同一个模板,但是模板只有在执行的时候才会实例化,这样就会导致同一个模板被多次实例化,造成大量不必要的开销。通过显式实例化的方式来降低这种开销。

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

再实例化声明的地方模板执行的时候不会实例化,只有在实例化定义的时候模板才会实例化。

//Application.cc
extern template class Blob<string>;
extern template int compare(const int&, const int &);

//templateBuild.cc
template int compare(const int &, const int &);
template class Blob<string>;

再编译的时候一定要将Application.otemplateBuild.o连接在一起。

16.2 模板实参推断

从函数实参来确定模板实参的过程叫做模板实参推断。

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

将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换。

可以将一个非const对象的引用传递给一个const引用形参。
如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。

template <typename T> T fobj(T, T);
template <typename T> T fref(const T&, const T&);

int main(int argc, char const *argv[])
{
  std::string s1("a value");
  const std::string s2("another value");
  fobj(s1, s2);   //const被忽略
  fref(s1, s2);

  int a[10], b[42];
  fobj(a, b);
  fref(a, b); //error,数组类型不匹配
  return 0;
}

使用相同模板参数的函数形参,传递过来的实参也必须是相同类型。如果想要进行正常的类型转换,可以将模板参数定义为不同的类型.

template <typename T> bool compare(T, T);
long lng;
compare(lng, 1024); //error,类型不匹配

template <typename A, typename B> 
int flexibleCompare(const A& v1, const B& v2);
flexibleCompare(lng, 1024);

16.2.2 函数模板显式实参

指定显式模板实参

如果没有任何函数实参类型来推断模板实参的类型,可以使用显示模板实参类推断。

template <typename T1, typename T2, typename T3>
T1 sum(T2,T3);
int i;
long lng;
auto val3 = sum<long long>(i, lng); //long long sum(int, long)

显式模板实参的类型推断是按照从左至右依次进行,不能搞错顺序。

//糟糕的设计,用户必须指定三个模板参数
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);
//无法推断前几个模板参数
auto val2 = alternative_sum<long long>(i, lng);
//right,显式地指定所有三个参数
auto val3 = alternative_sum<long long, int, long>(i, lng);

模板类型参数显式地指定地函数实参可以进行正常的类型转换。

compare<long>(lng, 1024); //right,实例化compare(long, long);
compare<int>(lng, 1024);  //right,实例化compare(int, int);

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

如果无法确定函数的返回类型,并且显式指定模板参数给用户带来负担,我们可以采用尾置返回类型的办法。

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

16.2.5 模板实参推断和引用

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

如果函数模板是一个普通类型的引用,只能传递给它一个左值,实参可以是const也可以不是。

template <typename T> void f1(T&);
int i;
const int ci = 5;
f1(i);  //i是int,模板参数类型也为int
f1(ci); //ci是一个const int 模板参数T是const int
f1(5);  //error: 传递给一个&参数必须是一个左值

template <typename T> void f2(const T&);
f2(i);
f2(ci);
f2(5);

右值引用的推断和左值引用推断类似。

引用折叠和右值引用参数

template <typename T> void f3(T &&);
f3(42);

如果将一个左值传递给T &&,如int类型,编译器会将T推断为int &类型。如果间接的创建一个引用的引用,会形成引用折叠,引用会折叠成一个普通的引用类型。比如

  • X& &X& &&X&& &都折叠成类型X&
  • X&& &&、折叠成X&&

因此如果一个函数参数是指向模板参数类型的右值引用,可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,函数参数被实例化为一个普通的左值引用

16.2.6 理解std::move

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

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

std::move(std::string("bye!"))的工作流程:

  • 推断出T的类型为string
  • remove_referencestring进行实例化
  • remove_reference<string>的type成员为string
  • move的返回类型为string&&
  • move的函数参数类型为string&&
  • 所以不需要static_cast进行强制类型转化

当move调用s1(一个左值)的时候,执行流程:

  • 推断出T的类型为string &
  • remove_referencestring&进行实例化
  • remove_reference的type成员为string
  • move的返回类型为string&&
  • move的函数参数类型t实例化为string& &&经过类型折叠为string &
  • 调用的实例化为string&& move(string &t)在使用强制类型转换为string&&

16.2.7 转发

某些函数需要将其实参连同类型不变地转发给其他函数,此时我们需要保持被转发实参的所有性质。

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

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

int j = 0;
f(42, j); //f改变了实参j
flip1(f, j, 42);  //通过flip1调用f不会改变j
return 0;

j传递给的是t2是一个普通的int,不会影响j。

定义能保持类型

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

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

j为左值,传递给T1,T1为int&,然后t1为int& &&t1,通过引用折叠t1变为int &t1,然后在通过引用绑定到v2。

使用forward来保持类型信息

forward必须通过显式模板实参来调用。forward返回该显示实参类型的右值引用。forward<T>的返回类型是T&&.
用一个指向模板参数类型的右值引用函数参数T&&时,forward会保持实参类型的所有细节。

16.3 重载与模板

template <typename T> 
std::string debug_rep(const T &t) {
  std::ostringstream ret;
  ret << t;
  return ret.str();
}

template <typename T>
std::string debug_rep(T *p) {
  std::ostringstream ret;
  ret << "pointer: " << p;
  if (p)
    ret << " " << debug_rep(*p);
  else 
    ret << " null pointer";
  return ret.str();
}
string s("hi");
cout << debug_rep(s) << endl;

通过精确匹配,此时只有第一个版本可以调用。

cout << debug_rep(&s) << endl;此时两个函数都可以调用,但是通过精确匹配,debug_rep(T *p)版本。
如果有多个可行的版本可以调用,选择特例化的版本。

对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。

16.4 可变参数模板

可变数目的参数成为参数包,模板参数包包含零个或多个模板参数,函数参数包包含零个或多个函数参数。在class... or typename...为函数参数包的写法。

template <typename T, typename...Args>
void foo(const T &t, const Args&...rest);
int i = 0; 
double d = 3.14;
std::string s = "how now brown cow";
foo(i, s, 42, d); //包含三个参数
foo(s, 42, "hi"); //包含两个参数
foo(d, s);  //包中有一个参数
foo("hi");  //空包

sizeof...可以计算出类型参数的数目。

16.4.2 包扩展

通过在模式右边放一个省略号来触发扩展操作。

template <typename T, typename... Args>
std::ostream & print(std::ostream &os, const T &t, const Args&... rest) { //扩展Args
  os << t << " ";
  return print(os, rest...);  //扩展rest
}

Args的扩展,编译器将模式const Arg&应用到模板参数包Args中的每个元素,每个类型都是const type&.

print(cout, i, s, 42);  //扩展包中有两个参数
//扩展模式实例化
ostream& print(ostream &, const int&, const string&, const int&);

第二个扩展等价于print(os, s, 42).

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

debug_rep(rest)...表示希望对rest中的每个元素都调用debug_rep

errorMsg(std::cerr, fcnName, code.num(), otherData, "other", item);
  //扩展结果
  print(std::cerr, debug_rep(fcnName), debug_rep(code.num()), debug_rep(ohterData), 
          debug_rep("otherData"), debug_rep(item));

但是如果是print(os, debug_rep(rest...))就会编译失败,因为没有函数debug_rep(fcname, code.num(), otherData, "otherData", item)

16.5 模板特例化

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

posted on 2022-03-02 12:32  翔鸽  阅读(38)  评论(0编辑  收藏  举报