面向对象编程和泛型编程都能处理编写程序时不知道类型的情况
区别就是OOP是在运行时处理,而泛型编程是在编译的时候就能知道类型了
一.定义模板
1.我们可以为函数定义一个模板,而不是为每一个类型定义一个函数。
比较函数:
#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;
}
模板定义以关键字template开始,
后面跟一个参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用< >包围起来。
当我们调用一个函数模板时,编译器会用函数的实参来为我们推断模板实参
2.类型参数
类型参数前必须使用关键字class或者typename
在模板参数列表中,这两个关键字的含义相同,可以互换使用,一个模板参数列表中可以同时使用这两个关键字
类型参数在函数中使用我们就把它当作正常类型使用就可以了,比如作为函数的参数,返回值,以及函数内部定义变量等等
3.非类型模板参数
除了定义类型参数,我们还可以定义非类型参数,非类型参数表示一个值而非一个类型,我们通过一个特定的类型名而非关键字class或typename来指定。
当一个模板被实例化,非类型参数被一个编译器推断的值所取代。
一个非类型参数可以是一个整形,或者是一个指向对象或函数类型的指针或(左值)引用,绑定到非类型整形参数必须是一个常量,绑定到指针或引用的非类型模板
参数的实参必须有静态生存期。
注意:非类型模板参数的模板必须是常量表达式
inline 和constexpr也可以修饰函数模板,放在返回值前面
4.编写类型无关的代码
编写泛型代码的两个重要的原则
<1.模板中的函数参数是const的引用
<2.函数体条件中判断仅仅使用<比较
这两个可以降低对类型的要求
比如const的引用,就保证了函数可以用于不能拷贝的类型。
<比较就可以用于大多数定义了<比较的类型。
结论:模板程序应该尽量减少对实参类型的要求
5.模板编译
模板编译和普通的编译不同,它是在使用实例化时编译器才生成代码,为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义,
因此和非模板代码不同,模板的头文件通常既包括声明又包括定义。
模板的设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。
模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。
6.大多数编译错误在实例化期间报告
保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。
例题:编写行为类似find算法的模板,函数需要两个模板类型参数,一个表示函数的迭代器参数,另外一个表示值的类型。
#include <iostream>
#include <string>
#include <list>
#include <vector>
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;
}
2.类模板
类模板是生成类的模板-。
它和函数模板的不同是它不能向函数模板一样推断出类型是什么,而是要自己指定类型是什么
比如vector<T>就是一个模板类,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;
}
<1.一个类模板的每个实例都形成一个独立的类,但是类和类之间没有关系。
<2.我们既可以在类模板外部为其定义成员函数,也可以在类模板内部定义,内部定义被隐式的声明为内敛函数。
<3.类模板的成员函数具有和模板相同的模板参数,因此,定义在模板之外的成员函数就必须以关键字template开始,后接参数列表.
<4.默认情况下,对于一个实例化的类模板,其成员只有在使用时才被实例化。
<5.当我们使用一个类模板类型必须提供模板参数,但有一个例外,在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。
模板类和友元
<6.类和友元各自是否是模板是无关的。友元关系被限定在用相同的类型实例化。
<7.一个模板类也可以指定另外一个模板类为友元或者另外一个模板类的特定实例
#include <iostream>
template <typename T>
class C
{
};
template <typename T>
class B
{
//friend class C<int>; //特定类为友元
template <typename X> friend class C; //模板类为友元
template <typename T> friend class C; //error:本模板类参数列表已经包含T了,友元不可再次包含T,需要换参数,否则会报错显示应藏参数
};
int main()
{}
<8.令自己的 实例化类型作为友元(c++11)
#include <iostream>
template <typename T, typename X>
class C
{
friend T;
};
int main()
{}
那么我们实例化的时候,实例化类型是A,A就是模板类的友元,A可以是类类型,或者是内置类型。
<9.类模板别名(c++11)
旧标准中,我们只能使用typedef 为特定模板类型定义类型别名。
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;
}
<10.类模板的static成员
当类模板包含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;
}
<11.使用模板时别偷懒,时刻注意模板声明template和参数列表,使用时记得注意指定类型,不明则使用T
3.模板参数
<1.模板参数和作用域
模板参数符合隐藏规则,而且一个模板参数名在参数列表中只能出现一次。
一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。
<2.使用类的类型成员
我们处理模板的时候,必须让编译器知道名字是否表示一个类型
T::size_type *p; //error typename T::size_type *p
我们不知道T是一个名字还是模板的参数列表里的类型,c++编译器默认为名字。
所以如果我们像上面想表示为一个类型的话必须显式的指定,在前面添加关键字typename
<3.默认模板实参
在新标准中,我们可以为函数和类模板提供默认实参
函数:
#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;
}
类:
#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
}
注意:
记住typename和class不是完全相同的,在指明是一个类型而不是名字的情况下必须用typename
比如typedef typename T::size_type = size_type
4.成员模板
<1.普通(非模板)类的成员模板
#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());
}
<2.类模板的成员模板
#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());
}
<3.实例化与成员模板
实例化一个类模板的成员函数模板,类模板必须明确指出类型,函数模板根据参数推断出类型,如上面main函数里面的实例。
5.控制实例化
我们知道一个模板在使用时根据不同的类型就会产生不同实例化。
如果在多个文件中使用模板,产生的多个实例化可能会重复,带来额外的开销影响效率。
我们可以通过显式实例化来解决问题
形式:
extern template declaration //实例化声明
template declaration //实例化定义
我们只需要在一个文件中实例化定义,其他文件实例化声明即可。
注意:对每个实例化声明,在程序中某个位置必须有其显式的实例化定义
实例化定义会实例化所有成员,包括内敛函数成员
所以在一个类模板定义中,所用类型必须能用于模板的所有成员函数
6.效率与灵活性
书上举的是shared_ptr 和unique_ptr 的例子
这两个智能指针都能绑定删除器
shared_ptr删除器:在运行时绑定的删除器
在一个shared_ptr的生存期中,我们可以随时改变其删除器的类型。
unique_ptr删除器:在编译时绑定的删除器
unique_ptr的删除器是unique_ptr的一部分
unique_ptr有两个模板参数,一个表示它所管理的指针,另一个表示删除器的类型,在编译时绑定,所以没有运行开销
template <typename T> class shared_ptr
template <typename T, class D = dedault_delete<T>> class unique_ptr
通过编译时绑定删除器,unique_ptr 避免了间接调用删除器的运行时开销。
通过运行时绑定删除器,shared_ptr 使用户重载删除器更加方便。
自己实现shared_ptr 和 unique_ptr
参考别人的实现 https://github.com/Mooophy/Cpp-Primer/tree/master/ch16/ex16.28
shared_ptr
#include <iostream>
#include <functional>
//声明
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;
}
unique_ptr
#include <iostream>
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);
}
二.模板实参推荐
1.类型转换与模板实参推荐
函数模板在传递实参时会根据实参生成对应参数的函数,但是我们需要注意一点就是只有有限的几种类型会应用于这些实参。
编译器通常不对实参进行类型转换,而是像上面说的生成新的对应参数的函数。
在类型转换中,能用于函数模板的有以下两种。
<1.const 转换:也就是非const类型的参数传递给const类型的形参,注意必须是引用或指针,如果不是引用或指针传递的就是拷贝。
<2.数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。
其他如算术转换,派生类向基类的转换,用户定义的转换都不能用于函数模板。
总结:
将实参传递给代模板类型的参数时,能够自动类型转换的只有const转换及数组或函数到指针的转换。
如果想要传递不同的参数,则我们必须自己定义那种不同的模板类型。
如果函数模板的参数不是模板则可以正常进行转换。
2.函数模板显式实参
有些时候我们必须显示的指定一些模板的参数
比如
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
这个函数中,我们返回是一个模板参数,在函数中,没有任何类型能推断出T1的类型,所以我们要显式指定
我们提供显式模板实参的方式与定义类模板实例的方式相同,显式模板实参在尖括号中给出,位于函数名之后,实参列表之前。
auto val = sum<long long>(t1, t2);
显式模板参数按照从左到右的顺序和对应的模板参数匹配,只有尾部最右参数的显示模板实参才可以忽略,前提是他们可以从函数参数推断出来。
如果我们把上面的改为
template <typename T1, typename T2, typename T3>
T3 sum(T2, T1);
就必须显式的指定三个参数的类型
auto val = sum<long long , long ,int >(t1, t2, t3);
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的type成员表示被引用的类型。
还有其他的类型转换模板
标准类型转换模板
<1.remove_reference
<2.add_const
<3.add_lvalue_reference
<4.add_rvalue_reference
<5.remove_pointer
<6.add_pointer
<7.make_signed
<8.make_unsigned
<9.remove_extent
<10.remove_all_extents
其他的使用都和remove_reference类似。每个模板都有一个type的public成员表示类型。
注意有时type不是元素的类型,比如指针,type就不是指针类型而是指针指向的元素类型。
4.函数指针和实参推荐
我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时。编译器会用指针的类型来推断模板
template <typename T>
T fun(T,T);
int (*p)(int,int) = fun;
那么p就指向了fun的int实例化版本。
当然也可以显式指定
fun<int>
注意:当参数是一个函数模板实例的地址时,程序上下文必须满则:对每个模板参数能唯一确定其类型或值。
5.模板实参推断和引用
<1.从左值引用函数参数推断类型
当一个函数类型参数是普通的左值引用时,规定只能传递给它一个左值(一个变量或一个引用的表达式),实参可以是const类型也可以不是。
如果实参是const类型,T将会被推断是const类型
如果函数类型参数是const&T时,我们可以传递任何类型的实参,一个对象,一个临时对象或者字面值常量。
<2.从右值引用函数参数推断类型
我们可以传递一个右值,推断规则类似左值引用。
左值不能绑定在右值引用上,但是有两个例外规则。
第一个:当我们将一个左值传递给函数的右值引用参数时,编译器推断模板类型参数为实参的左值引用类型。注意是引用。
比如传递给模板函数参数T &&一个int i,推断出来的类型就是int &.
第二个:如果我们间接创建一个引用的引用就会形成引用折叠,引用折叠在大部分情况下会形成一个普通的左值引用,但是在右值引用的引用情况下
会生成右值引用。
X& &, X& &&和X&& &都折叠成X&
X&& &&折叠成X&&
注意:引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。
这两个规则导致了两个重要的结果:
1.如果一个函数参数是一个指向模板类型蚕食的右值引用,则它可以被绑定到一个左值上。
2.如果实参是一个左值,则推断出的模板实参类型是一个左值引用,且函数参数将被实例化一个左值引用参数。
注意:如果一个函数参数是指向模板参数类型的右值引用,则可以传递给它任何类型的实参,如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通
左值引用。
<3.编写接受右值引用参数的模板函数
使用右值引用的函数模板应该向我们之前所写的一样
定义两种。
template <typename T> void f(T&&);
template <typename T> void f(const T&);
可避免一些错误。
总之:泛型模板编程要细心,时刻注意参数问题。
模板参数是右值引用,传递右值引用,推断出是T
模板参数是右值引用,传递左值,推断出是左值引用。
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到一个右值引用是允许的。
7. 转发
某些函数需要将一个或多个实参连同类型传递给其他的函数,这种情况下我们需要保证实参的类型。
举个例子来说明
#include <iostream>
#include <unility>
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 的值。
改进有两个方面:
1. 参数变成右值引用
通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。
引用类型中const是底层的,我们可以保证const属性,通过引用折叠我们可以保持翻转实参的左值/右值属性。
结论:如果一个函数参数是指向模板类型参数的右值引用, 它对应的实参的const 属性和左值/右值属性得到保持。
2. 使用std::forward:如果参数不是一个左值引用,返回右值,如果参数是一个左值引用,返回该返回的类型。
头文件#include <utility>
当传递一个右值引用时,我们知道右值引用也是一个左值表达式。在调用g时就会出错,不能将左值绑定到右值上。
我们可以使用一个forward的新标准库来传递参数,它能保持原始实参的类型。
forward必须通过显式模板参数来调用。forward返回该显式实参类型的右值引用
通常情况下我们使用forward传递那些定义为模板类型参数的右值引用的函数参数,通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性
上面的改进后函数,如果实参是T1一个右值int 传递func,那么T1推断出是int 类型,std::forward<T1>将int 变为int&&.
如果实参是一个左值, 通过引用折叠,T本身就是一个左值int&。
顺便贴一下cppman手册里的例子,方便理解
第一个给fn传递a,a是一个左值,fn的形参是右值引用,前面说过给右值引用传递左值,会变成左值引用,overloaded(x)调用的是左值版本。
overloaded(std::forward<T>(x))x是一个左值引用时,返回一个左值引用,也掉用的是左值的overloaded。
第二个给fn传递0,0是一个右值,fn的形参是右值引用,T推断出是int类型,右值引用也是一个左值,所以调用左值版本,但是使用std::forward时,
上面说了rvalue if argument is rvalue,所以调用右值版本。std::forward<T>(x)返回一个右值。
三. 重载和函数模板
函数模板可以被另一个模板或一个普通非模板函数重载。与往常一样,名字相同的函数必须具有不同数量或类型的参数。
与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。
当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本。我理解特例话指的是小众,比如两个模板都实例化出了适配的实例,第一个模板作用范围大(比如有const修饰,就可以作用const和non-const),第二个模板作用范围小,那么第二个模板产生的实例就是最特例化的版本。
对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本。
四. 可变参数模板(c++11)
一个可变参数就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);
与往常一样,编译器从函数的实参推断模板参数类型。对于一个可变参数模板,编译器还会推断包中参数的数目。
#include <iostream>
#include <string>
template <typename T, typename ... Args>
void foo(const T &t, const Args& ... rest)
{
std::cout << sizeof...(Args) << std::endl;
std::cout << sizeof...(rest) << std::endl;
}
int main()
{
int i = 10;
double d = 1.1;
std::string s = "ss";
foo(i, d, s);
}
result: 2
2
定义就如同例子里面定义一样,sizeof...当我们需要知道包中有多少个元素的时候就可以使用sizeof...
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;
}
从打印逗号的情况我们可以看出最后一个调用的是两参版本。其他几次都调用的是三参版本
是一个递归的执行过程,每次打印一个参数,包中就少一个参数,直到打印最后一个参数时,我们必须定义两参版本。
两参版本负责终止递归并打印初始调用中的最后一个实参。
注意:当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中,否则,可变参数版本会无限递归。
好的做法是都在最开始声明。
5. 模板特例化(特化)
所谓模板特化就是针对模板特殊情况特殊处理。
模板实例化是在第一次使用时,编译器会按确定的类型生成对应的实例模板。
我们可以定义函数模板和类模板的特例化
特例化本质是一个模板,而非重载,所以特例化本质不影响函数匹配。
模板及其特例化应该声明到同一个头文件中,所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。
类模板部分特例化
注意:我们只能部分特例化类模板,而不能部分特例化函数模板。