16.1 定义模板
- 面向对象编程和泛型编程都能处理编写程序时不知道类型的情况区别就是OOP是在运行时处理,而泛型编程是在编译的时候就能知道类型了。
16.1.1 函数模板
- 模板定义以关键字template开始,后面跟一个参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用< >包围起来。
- 当我们调用一个函数模板时,编译器会用函数的实参来为我们推断模板实参
#include <iostream>
template <typename T>
int compare(const T&v1, const T&v2)
{
if(v1 < v2)
return -1;
if(v1 > v2)
return 1;
return 0;
}
int main()
{
int i1 = 10, i2 = 20;
double d1 = 20.1, d2 = 20.01;
unsigned u1 = 10, u2 = 10;
std::cout << compare(i1, i2) << std::endl;
std::cout << compare(d1, d2) << std::endl;
std::cout << compare(u1, u2) << std::endl;
}
- 类型参数前必须使用关键字class或者typename,在模板参数列表中,这两个关键字的含义相同,可以互换使用,一个模板参数列表中可以同时使用这两个关键字。
// 正确:返回类型和参数类型相同
template <typename T> T foo(T* p){
T tmp = *p; // tmp的类型将是指针p指向的类型
// ...
return tmp;
}
// 错误:U之前必须加上class或typename
template <typename T, U> T calc(const T&, const U&);
- 除了定义类型参数,我们还可以定义非类型参数,非类型参数表示一个值而非一个类型,我们通过一个特定的类型名而非关键字class或typename来指定。当一个模板被实例化,非类型参数被一个编译器推断的值所取代。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。
- 一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用,绑定到非类型整形参数必须是一个常量,绑定到指针或引用的非类型模板。注意:非类型模板参数的模板必须是常量表达式。
- inline 和constexpr也可以修饰函数模板,放在返回值前面。
- 编写泛型代码的两个重要的原则
- 模板中的函数参数是const的引用
- 函数体条件中判断仅仅使用<比较
- 模板程序应尽量减少对实参类型的要求
- 我们将类定义和函数声明放在头文件,将普通函数和类的成员的定义放在源文件中。但是函数模板和类模板成员函数的定义通常放在头文件中。
- 模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。
- 保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。
#include <iostream>
#include <string>
#include <list>
#include <vector>
// 编写行为类似find算法的模板,函数需要两个模板类型参数,一个表示函数的迭代器参数,另外一个表示值的类型。
template<typename iterator, typename T>
iterator Find(const iterator &beg, const iterator &end, const T&ele)
{
for(auto it = beg; it != end; ++it)
{
if(*it == ele)
return it;
}
return end;
}
int main()
{
std::vector<int>ivec1 = {1,2,3,4,5,6,7,8,9,0};
auto ret = Find(begin(ivec1), end(ivec1), 3);
if(ret == end(ivec1))
std::cout << "not find" << std::endl;
else
std::cout << "find " << *ret << std::endl;
std::list<std::string>il = {"111","222","333","444","555"};
auto ret2 = Find(il.begin(), il.end(), "333");
if(ret2 == il.end())
std::cout << "not find" << std::endl;
else
std::cout << "find " << *ret2 << std::endl;
}
16.1.2 类模板
- 类模板是生成类的模板,它和函数模板的不同是它不能向函数模板一样推断出类型是什么,而是要自己指定类型是什么比如vector就是一个模板类,T可以是任何类型int ,double ,string或者是自己定义的类类型。
#ifndef _BLOB_H_
#define _BLOB_H_
#include <iostream>
#include <vector>
#include <string>
#include <memory>
#include <initializer_list>
#include <stdexcept>
#include "strblob.h"
//必须声明
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&>);
public:
typedef T value_type;
//加上typename 是因为T是一个模板类型,编译器不知道std::vector<T>::size_type 是什么,加上typename就是告诉编译器是一个type
typedef typename std::vector<T>::size_type size_type;
blob();
blob(std::initializer_list<T> il);
size_type size()const { return data->size(); }
bool empty()const { return data->empty(); }
void push_back(const T&t) { return data->push_back(t); }
void push_back(T &&t) { return data->push_back(std::move(t)); }
void pop_back();
T& back();
T& front();
T& operator[] (size_type i);
const T& operator[](size_type i)const;
private:
void check(size_type i, const std::string &s)const;
std::shared_ptr<std::vector<T>> data;
};
#endif
#include "blob.h"
//类模板的成员函数必须具有和模板相同的模板参数
template<typename T>
blob<T>::blob():data(std::make_shared<std::vector<T>>()) { }
template<typename T>
blob<T>::blob(std::initializer_list<T> il):
data(std::make_shared<std::vector<T>>(il)) { }
template<typename T>
void blob<T>::check(size_type i, const std::string &s)const
{
if(i >= data->size())
throw std::out_of_range(s);
}
template<typename T>
void blob<T>::pop_back()
{
check(0, "pop_back on empty blob");
data->pop_back();
}
template<typename T>
T& blob<T>::front()
{
check(0, "back on empty blob");
return data->front();
}
template<typename T>
T& blob<T>::back()
{
check(0, "back on empty blob");
return data->back();
}
template<typename T>
T& blob<T>::operator[](blob<T>::size_type i)
{
check(i, "subscript out of range");
return static_cast<T>(const_cast<const blob<T>>(*this)[i]);
}
template<typename T>
const T& blob<T>::operator[](blob<T>::size_type i)const
{
check(i, "subscript out of range");
return (*data)[i];
}
#ifndef _STRBLOB_H_
#define _STRBLOB_H_
#include <iostream>
#include <memory>
#include <vector>
#include <string>
#include <stdexcept>
#include "blob.h"
//必须要其他的.h文件声明,否则无法使用
template<typename T> class blob;
template<typename T>
class blobptr
{
public:
blobptr():curr(0){ }
blobptr(blob<T> &a, std::size_t sz = 0):
wptr(a.data), curr(sz) { }
T& operator*()const
{
auto p = check(curr, "* error");
return (*p)[curr];
}
blobptr& operator++();
blobptr& operator--();
private:
std::shared_ptr<std::vector<T>>
check(std::size_t i, const std::string &msg);
std::size_t curr;
std::weak_ptr<std::vector<T>>wptr;
};
#endif
#include "strblob.h"
template<typename T>
std::shared_ptr<std::vector<T>>
blobptr<T>::check(std::size_t i, const std::string &msg)
{
//检查是否存在
auto ret = wptr.lock();
if(!ret)
throw std::runtime_error("unbound blobptr");
if(i < 0 && i >= ret->size())
throw std::out_of_range(msg);
return ret;
}
template<typename T>
blobptr<T>& blobptr<T>::operator++()
{
blobptr ret = *this;
++*this;
return ret;
}
template<typename T>
blobptr<T>& blobptr<T>::operator--()
{
blobptr ret = *this;
--*this;
return ret;
}
- 一个类模板的每个实例都形成一个独立的类。类型Blob<string>与任何其他Blob类型都没有关联,也不会对任何其他Blob类型的成员有特殊的访问权限。
- 我们既可以在类模板外部为其定义成员函数,也可以在类模板内部定义,内部定义被隐式的声明为内联函数。
- 类模板的成员函数具有和模板相同的模板参数,因此,定义在模板之外的成员函数就必须以关键字template开始,后接参数列表。
- 默认情况下,对于一个实例化的类模板,其成员只有在使用时才被实例化。
- 当我们使用一个类模板类型必须提供模板参数,但有一个例外,在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。
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:
// 若检查成功,check返回一个指向vector的智能指针
std::share_ptr<std::vector<T>> check(std::size_t, const std::string&) const;
// 保存一个weak_ptr,表示底层vector可能被销毁
std::weak_ptr<std::vector<T>> wptr;
std::size_t curr;
- 在类模板外定义成员时,我们并不在类的作用域中,直到遇到类名才表示进入类的作用域。
// 等价于
BlobPtr<T> ret = *this;
- 类和友元各自是否是模板是无关的。友元关系被限定在用相同的类型实例化。如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例,如果友元本身是模板,类可以是授权所有友元模板实例,也可以只授权给特定实例。
- 一个模板类也可以指定另外一个模板类为友元或者另外一个模板类的特定实例
template <typename> class BlobPtr;
template <typename> class Blob;
template <typename T> bool operator==(const Blob<T>&, const Blob<T>&);
template <typename T> class Blob {
// 每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符
friend class BlobPtr<T>;
friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
}
- 为了让所有实例都成为友元,友元声明中必须使用与类模板本身不同的模板参数。
- 令自己的 实例化类型作为友元(c++11)
#include <iostream>
template <typename T, typename X>
class C
{
friend T;
};
int main()
{}
- c++11标准中,我们可以为模板类定义模板类型别名,而且多个参数时可以指定参数是T或者是特定类型。
#include <iostream>
template <typename T, typename X, typename Z>
class C
{
};
//template参数列表指定参数的个数,但是我们能指定一个或多个特定类型比如TEP2的int
template <typename T, typename X, typename Z> using TEP = C<T, X, Z>;
template <typename T, typename X> using TEP2 = C<T, X, int>;
int main()
{
C<int, int, int> c;
TEP<int, int, int> t;
TEP2<int, int> t2;
}
- 当类模板包含static成员时(数据或者是函数),每个类模板的实例都有一个static成员,而且定义类模板的static成员时,前面必须加template <参数列表>
#include <iostream>
template <typename T>
class C
{
public:
static void fun(T &a);
private:
static int i;
};
template <typename T>
void C<T>::fun(T &a)
{
std::cout << a << std::endl;
}
//别忘记了int
template <typename T>
int C<T>::i = 0;
int main()
{
C<int>c;
}
16.1.3 模板参数
- 模板参数符合隐藏规则,而且一个模板参数名在参数列表中只能出现一次。
- 一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。
- 模板声明必须包含模板参数,与函数参数相同,声明中模板参数的名字不必与定义中相同。
- 我们处理模板的时候,必须让编译器知道名字是否表示一个类型,我们不知道T是一个名字还是模板的参数列表里的类型,c++编译器默认为名字。使用typename通知编译器一个名字表示类型。
T::size_type *p; //error typename T::size_type *p
- 在新标准中,我们可以为函数和类模板提供默认实参
#include <iostream>
#include <functional>
// X默认实参是std::less<T>这个函数对象
template <typename T, typename X = std::less<T>>
int compare(const T &v1, const T &v2, X x = X())
{
if(x(v1, v2))
{
std::cout << v1 << " " << v2 << std::endl;
return -1;
}
if(x(v2, v1))
{
std::cout << v1 << " " << v2 << std::endl;
return 1;
}
return 0;
}
int main()
{
//传参数函数对象std::greater
std::cout << compare(0, 1, std::greater<int>()) << std::endl;
//使用默认参数 std::less
std::cout << compare(0, 1) << std::endl;
//使用函数
Sales_data item(cin), item(cin);
std::cout << compare(item1, item2, compareIsbn);
}
#include <iostream>
template <typename T = int>
class C
{
public:
C(T v = 0):
val(v) { }
private:
T val;
};
int main()
{
C<> c; //使用默认int
C<double> c2; //覆盖默认int改用double
}
16.1.4 成员模板
- 成员模板不能是虚函数。
- 普通(非模板)类的成员模板
#include <iostream>
#include <memory>
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;
};
int main()
{
double *p = new double;
DebugDelete d;
d(p);
int *i = new int;
DebugDelete()(i); // 对于模板函数可以自动进行匹配,对于模板类不行
std::unique_ptr<int , DebugDelete> p2(new int, DebugDelete());
// 注意尖括号里的是类型,括号里的是可调用对象
}
- 类模板的成员模板==(这里没看懂!!)==
#include <iostream>
#include <vector>
template <typename T>
class C
{
public:
template <typename X>C(X b, X e);
private:
std::vector<T>ivec;
};
template <typename T> //类的参数列表
template <typename X> //构造函数的参数列表
C<T>::C(X b, X e)
{
}
int main()
{
std::vector<int>svc = {1,2,3,4,5};
C<int>c(svc.begin(), svc.end());
}
template <typename T> // 类的类型参数
template <typename It> // 构造函数的类型参数
Blob<T>::Blob(It b, It e):
data(std::make_shared<std::vector<T>>(b, e)) {}
16.1.5 控制实例化
- 我们知道一个模板在使用时根据不同的类型就会产生不同实例化。如果在多个文件中使用模板,产生的多个实例化可能会重复,带来额外的开销影响效率。我们可以通过显式实例化来解决问题。
extern template declaration //实例化声明
template declaration //实例化定义
- 没有进行声明的都进行了隐式实例化,进行了声明的必须在程序中的某个位置进行显式实例化。
- 我们只需要在一个文件中实例化定义,其他文件实例化声明即可。注意:对每个实例化声明,在程序中某个位置必须有其显式的实例化定义
- 实例化定义会实例化所有成员,包括内敛函数成员。所以在一个类模板定义中,所用类型必须能用于模板的所有成员函数。
16.1.6 效率与灵活性
- shared_ptr 和unique_ptr这两个智能指针都能绑定删除器
- shared_ptr删除器:在运行时绑定的删除器。在一个shared_ptr的生存期中,我们可以随时改变其删除器的类型。
- unique_ptr删除器:在编译时绑定的删除器。unique_ptr有两个模板参数,一个表示它所管理的指针,另一个表示删除器的类型,在编译时绑定,所以没有运行开销。
template <typename T> class shared_ptr
template <typename T, class D = dedault_delete<T>> class unique_ptr
- 自己实现shared_ptr 和 unique_ptr
#include <iostream>
#include <functional>
// share_ptr
//声明
template <typename> class Myshared_ptr;
template <typename T> void swap(Myshared_ptr<T>&lhs, Myshared_ptr<T>&rhs);
//删除器
class DebugDelete
{
public:
DebugDelete(std::ostream&s = std::cerr):
os(s) { }
template <typename T>
void operator()(T *p)
{
os << "delete~" << std::endl;
delete p;
}
private:
std::ostream &os;
};
template <typename T>
class Myshared_ptr
{
//自定义交换函数
friend void swap(Myshared_ptr<T>&lhs, Myshared_ptr<T>&rhs);
public:
Myshared_ptr():
p(nullptr), num(new std::size_t(0)) { }
//explicit避免隐式转换
explicit Myshared_ptr(T *t, std::function<void(T*)>d = DebugDelete()):
p(t), num(new std::size_t(1)), deleter(d) { }
Myshared_ptr(const Myshared_ptr& mp):
p(mp.p), num(mp.num), deleter(mp.deleter)
{ ++*num; }
Myshared_ptr(Myshared_ptr &&mp)noexcept;
Myshared_ptr& operator=(const Myshared_ptr &);
Myshared_ptr& operator=(Myshared_ptr &&)noexcept;
operator bool()const
{ return p ? true : false; }
T& operator*()
{ return *p; }
T* operator->()
{ return & this->operator*(); }
std::size_t use_count()
{ return *num; }
T* get()const noexcept
{ return p; }
bool unique()const noexcept
{ return *num == 1; }
void swap(Myshared_ptr &mp)
{ ::swap(*this, mp); }
void reset()noexcept
{
decrement_n_destroy();
}
void reset(T *t)
{
if(p == t)
{
decrement_n_destroy();
p = t;
num = new std::size_t(1);
}
}
~Myshared_ptr()
{
decrement_n_destroy();
}
private:
void decrement_n_destroy();
private:
T *p = nullptr;
std::size_t *num = nullptr;
//删除器,使用function是为了函数,函数指针,函数对象和lambda都可以作为删除器类型。
std::function<void(T*)>deleter{DebugDelete()};
};
template <typename T>
inline void
swap(Myshared_ptr<T>&lhs, Myshared_ptr<T>&rhs)
{
using std::swap;
swap(lhs.p, rhs.p);
swap(lhs.num, rhs.num);
swap(lhs.deleter, rhs.deleter);
}
template <typename T>
inline Myshared_ptr<T>::Myshared_ptr(Myshared_ptr<T> &&mp)noexcept:
p(mp.p), num(mp.num), deleter(std::move(mp.deleter))
{
mp.p = nullptr;
mp.num = nullptr;
}
template <typename T>
inline Myshared_ptr<T>&
Myshared_ptr<T>::operator=(const Myshared_ptr<T> &mp)
{
++*mp.num;
decrement_n_destroy();
p = mp.p;
num = mp.num;
deleter = mp.deleter;
return *this;
}
template <typename T>
inline Myshared_ptr<T>&
Myshared_ptr<T>::operator=(Myshared_ptr<T> &&mp)noexcept
{
decrement_n_destroy();
::swap(*this, mp);
return *this;
}
template <typename T>
inline std::ostream&
operator<<(std::ostream &os, Myshared_ptr<T>&p)
{
os << p.get();
return os;
}
template <typename T>
inline void
Myshared_ptr<T>::decrement_n_destroy()
{
if(p)
{
if(--*num == 0)
{
delete num;
deleter(p);
}
num = nullptr;
p = nullptr;
}
}
int main()
{
Myshared_ptr<int>p1;
Myshared_ptr<int>p2(new int(10));
p1 = p2;
std::cout << p1.use_count() << std::endl;
}
#include <iostream>
// unique_ptr
template <typename T, typename> class Myunique_ptr;
template <typename T, typename D>
void swap(Myunique_ptr<T,D> &lhs, Myunique_ptr<T,D> &rhs);
class default_delete
{
public:
default_delete(std::ostream &s = std::cerr):
os(s) { }
template <typename T>
void operator()(T *p)
{
os << "delete ptr" << std::endl;
delete p;
}
private:
std::ostream &os;
};
template <typename T, typename D = default_delete>
class Myunique_ptr
{
friend void swap(Myunique_ptr<T,D>&lhs, Myunique_ptr<T,D>&rhs);
public:
// preventing copy and assignment
Myunique_ptr(const Myunique_ptr<T> &) = delete;
Myunique_ptr& operator=(const Myunique_ptr<T> &) = delete;
Myunique_ptr() = default;
// explicit 禁止隐式转换
explicit Myunique_ptr(T *t):
p(t) { }
// 左值版本
Myunique_ptr(Myunique_ptr<T> &&mp)noexcept
:p(mp.p) { mp.p = nullptr; }
// 在对象即将要销毁时可以复制
Myunique_ptr& operator=(Myunique_ptr &&mp)noexcept;
Myunique_ptr& operator=(std::nullptr_t n)noexcept;
T& operator*()const
{ return *p; }
T* operator->()const
{ return & this->operator*(); }
operator bool() { return p ? true : false; }
T* get()const noexcept
{ return p; }
void swap(Myunique_ptr<T,D> &rhs)
{
::swap(*this, rhs);
}
void reset() noexcept { deleter(p); p = nullptr; }
void reset(T *t)noexcept { deleter(p); p = t; }
T* release();
~Myunique_ptr()
{ deleter(p); }
private:
T *p;
D deleter = D();
};
template <typename T, typename D>
inline void
swap(Myunique_ptr<T,D>&lhs, Myunique_ptr<T,D>&rhs)
{
using std::swap;
swap(lhs.p, rhs.p);
swap(lhs.deleter, rhs.deleter);
}
template <typename T, typename D>
inline Myunique_ptr<T,D>&
Myunique_ptr<T,D>::operator=(Myunique_ptr &&mp)noexcept
{
if(this->p != mp.p)
{
deleter(p);
p = nullptr;
::swap(*this, mp);
}
return *this;
}
template <typename T, typename D>
inline Myunique_ptr<T,D>&
Myunique_ptr<T,D>::operator=(std::nullptr_t n)noexcept
{
if(n == nullptr)
{
deleter(p);
p = nullptr;
}
return *this;
}
template <typename T, typename D>
inline T*
Myunique_ptr<T,D>::release()
{
T* ret = p;
p = nullptr;
return ret;
}
int main()
{
int *i = new int(20);
Myunique_ptr<int>p(i);
}
16.2 模板实参推断
16.2.1 类型转换与模板类型参数
- 函数模板在传递实参时会根据实参生成对应参数的函数,但是我们需要注意一点就是只有有限的几种类型会应用于这些实参。编译器通常不对实参进行类型转换,而是像上面说的生成新的对应参数的函数。
- 在类型转换中,能用于函数模板的有以下两种:
- const 转换:也就是非const类型的参数传递给const类型的形参,注意必须是引用或指针,如果不是引用或指针传递的就是拷贝。
- 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。
- 如果想要传递不同的参数,则我们必须自己定义那种不同的模板类型。
- 如果函数模板的参数不是模板则可以正常进行转换。
16.2.2 函数模板显示实参
- 有些时候我们必须显示的指定一些模板的参数,比如:
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
- 这个函数中,我们返回是一个模板参数,在函数中,没有任何类型能推断出T1的类型,所以我们要显式指定,我们提供显式模板实参的方式与定义类模板实例的方式相同,显式模板实参在尖括号中给出,位于函数名之后,实参列表之前。
auto val = sum<long long>(t1, t2); // 返回类型是long long型
- 正常类型转换应用于显示指定实参。对于模板类型参数已经显示指定了的函数实参,也进行正常的类型转换。
long lng;
compare(lng, 1024); // 错误:模板参数不匹配
compare<long>(lng, 1024); // 正确:实例化compare(long, long)
compare<int>(lng, 1024); // 正确:实例化compare(int, int)
16.2.3 尾置返回类型与类型转换
#include <iostream>
#include <string>
#include <vector>
template <typename T>
auto func(T Beg, T End)->decltype(*Beg)
{
return *(Beg+(End-Beg)/2); // 返回序列中一个元素的引用(因为*迭代器返回的就是元素的引用)
}
int main()
{
std::vector<std::string>svec = {"111","222","333"};
std::cout << func(svec.begin(), svec.end()) << std::endl;
return 0;
}
- 但是上述代码有一个限制,传参是迭代器,则我们返回的只能是元素的引用而不是元素。为了获取元素类型,可以使用标准库的类型转换。头文件#include <type_traits> 这个头文件常用于模板元程序设计。但是类型转换模板在普通编程中也很有用,上面问题可以用remove_reference来解决。
auto func(T Beg, T End)->
typename std::remove_reference<decltype(*Beg)>::type; //因为是类型所以要加typename,前面说过,否则不能区分是类型还是变量
- 如果我们实例化remove_reference<int&>,则type将表示int类型。
- 标准类型转换模板,其他的使用都和remove_reference类似。每个模板都有一个type的public成员表示类型。注意有时type不是元素的类型,比如指针,type就不是指针类型而是指针指向的元素类型。
- remove_reference
- add_const
- add_lvalue_reference
- add_rvalue_reference
- remove_pointer
- add_pointer
- make_signed
- make_unsigned
- remove_extent
- remove_all_extents
16.2.4 函数指针和实参推断
- 我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时。编译器会用指针的类型来推断模板。
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); // 错误:使用compare的哪个实例?
func(compare<int>); // 正确
- 注意:当参数是一个函数模板实例的地址时,程序上下文必须满则:对每个模板参数能唯一确定其类型或值。
16.2.5 模板实参推断和引用
1 从左值引用函数参数推断类型
- 当一个函数类型参数是普通的左值引用时,规定只能传递给它一个左值(一个变量或一个引用的表达式),实参可以是const类型也可以不是。如果实参是const类型,T将会被推断是const类型。如果函数类型参数是const&T时,我们可以传递任何类型的实参,一个对象,一个临时对象或者字面值常量。
2 从右值引用函数参数推断类型
- 我们可以传递一个右值,推断规则类似左值引用。
- 左值不能绑定在右值引用上,但是有两个例外规则。
- 第一个:当我们将一个左值传递给函数的右值引用参数时,编译器推断模板类型参数为实参的左值引用类型。注意是引用。比如传递给模板函数参数T &&一个int i,推断出来的类型就是int &.
- 第二个:如果我们间接创建一个引用的引用就会形成引用折叠,引用折叠在大部分情况下会形成一个普通的左值引用,但是在右值引用的引用情况下会生成右值引用。即对于给定类型的X,有:X& &, X& &&和X&& &都折叠成X&;X&& &&折叠成X&&
- 注意:引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。
- 这两个规则导致了两个重要的结果:
- 如果一个函数参数是一个指向模板类型参数的右值引用,则它可以被绑定到一个左值上。
- 如果实参是一个左值,则推断出的模板实参类型是一个左值引用,且函数参数将被实例化一个左值引用参数(T&)。
3 编写接受右值引用参数的函数模板
- 使用右值引用的函数模板应该向我们之前所写的一样,定义两种。
template <typename T> void f(T&&); // 绑定非const右值
template <typename T> void f(const T&); // 左值和const右值
16.2.6 理解std::move
// 标准库定义的std::move
template <typename>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
- std::move的参数是T&& t,是一个指向模板类型的右值引用,通过引用折叠可以和任何类型的参数匹配。
- 从一个左值static_cast到一个右值引用是允许的。
16.2.7 转发
- 某些函数需要将一个或多个实参连同类型传递给其他的函数,这种情况下我们需要保证实参的类型。
#include <iostream>
#include <unility>
// 函数接受一个可调用的表达式和两个额外的实参的模板。
// 对“翻转”的参数调用给定的可调用对象
// func是一个不完整的实现;顶层const和引用丢失了
template <typename F, typename T1, typename T2>
void func(F f, T1 t1, T2 t2)
{
f(t1, t2);
}
void f(int v1, int &v2)
{
}
void g(int &&v1, int &v2)
{
}
int main()
{
int i = 10, j = 20;
fun(f, i, j);
}
//改进后
template <typename F, typename T1, typename T2>
void func(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T1>(t1), std::forward<T2>(t2));
}
- 第一个函数,在fun里面调用f,f第二个参数是一个引用,说明我们希望在f 函数中改变j的值,但是在fun传递参数的时候就发生了拷贝,所以我们传递给f 的参数也是一份拷贝,修改了拷贝实际上没有修改到j 的值。
- 改进方法:
- 通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。引用类型中const是底层的,我们可以保证const属性,通过引用折叠我们可以保持翻转实参的左值/右值属性。结论:如果一个函数参数是指向模板类型参数的右值引用, 它对应的实参的const 属性和左值/右值属性得到保持。
- 使用std::forward:如果参数不是一个左值引用,返回右值,如果参数是一个左值引用,返回该返回的类型。头文件#include <utility>。当传递一个右值引用时,我们知道右值引用也是一个左值表达式。在调用g时就会出错,不能将左值绑定到右值上。我们可以使用一个forward的新标准库来传递参数,它能保持原始实参的类型。forward必须通过显式模板参数来调用。forward返回该显式实参类型的右值引用。
- 第一个给fn传递a,a是一个左值,fn的形参是右值引用,前面说过给右值引用传递左值,会变成左值引用,overloaded(x)调用的是左值版本。overloaded(std::forward(x))x是一个左值引用时,返回一个左值引用,也调用的是左值的overloaded。
- 第二个给fn传递0,0是一个右值,fn的形参是右值引用,T推断出是int类型,右值引用也是一个左值,所以调用左值版本,但是使用std::forward时,上面说了rvalue if argument is rvalue,所以调用右值版本。std::forward(x)返回一个右值。
16.3 重载与模板
- 函数模板可以被另一个模板或一个普通非模板函数重载。与往常一样,名字相同的函数必须具有不同数量或类型的参数。与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。
- 与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数
- 当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本。我理解特例话指的是小众,比如两个模板都实例化出了适配的实例,第一个模板作用范围大(比如有const修饰,就可以作用const和non-const),第二个模板作用范围小,那么第二个模板产生的实例就是最特例化的版本。
- 对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
- 在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本。
16.4 可变参数模板
- 一个可变参数就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。
// Args是一个模板参数包;rest是一个函数参数包
// Args表示零个或多个模板类型参数
// rest表示零个或多个函数参数
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);
2. 当我们需要知道包中有多少元素时,可以使用sizeof…运算符。
template<typename...Args> void g(Args...args) {
cout << sizeof...(Args) << endl; // 类型参数的数目
cout << sizeof...(args) << endl; // 函数参数的数目
}
16.4.1 编写可变参数函数模板
- 以前说过initializer_list定义一个可变数目参数的函数,但是类型是指定的。当我们既不知道参数的数目和参数的类型时,可变参数函数是很有用的。可变参数函数通常是递归的,第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
#include <iostream>
#include <string>
template <typename T, typename... Args>
std::ostream& print(std::ostream &os, const T&t, const Args&... rest);
template <typename T>
std::ostream& print(std::ostream &os, const T&t);
template <typename T, typename... Args>
std::ostream& print(std::ostream &os, const T&t, const Args&... rest)
{
os << t << ",";
return print(os, rest...);
}
template <typename T>
std::ostream& print(std::ostream &os, const T &t)
{
os << t;
return os;
}
int main()
{
int i = 10;
char c = 'c';
double d = 1.1;
std::string s = "ss";
print(std::cout, 1) << std::endl;
print(std::cout, 1,2) << std::endl;
print(std::cout, 1,2,3,4,5) << std::endl;
print(std::cout, i, c, d, s) << std::endl;
}
16.4.2 包扩展
- 扩展中的模式会独立应用于包中的每一个元素。
16.4.3 转发参数包(没太看懂!!)
- 组合使用可变参数模板与forward机制来编写函数,实现其实参不变地传递给其他函数。
class StrVec {
public:
template <class...Args> void emplace_back(Args&&...);
// 其他成员定义不变
}
template <class...Args>
inline
void StrVec::emplace_back(Args&&...args){ // 这样可以传递可变类型和可变数量的任意参数
chk_n_alloc(); // 如果需要的话重新分配内存空间
alloc.construct(first_free++, std::forward<Args>(args)...);
}
16.5 模板特例化(没太看懂!!)
- 所谓模板特化就是针对模板特殊情况特殊处理。
- 模板实例化是在第一次使用时,编译器会按确定的类型生成对应的实例模板。我们可以定义函数模板和类模板的特例化。特例化本质是一个模板,而非重载,所以特例化本质不影响函数匹配。
- 模板及其特例化应该声明到同一个头文件中,所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。
- 类模板部分特例化。注意:我们只能部分特例化类模板,而不能部分特例化函数模板。