template <typename T>
int compare(const T &v1, const T &v2)
{
if(v1 < v2) return -1;
if(v2 < v1) return 1;
return 0;
}
除了定义类型参数,还可以在模板中定义非类型参数。一个非类型参数表示一个值而非一个类型。通过一个特定的类型名而非关键字class
或typename
来指定非类型参数。当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所替代。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。
//定义了两个非类型参数,分别表示第一个和第二个数组的长度
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}
//调用时编译器会使用字面常量的大小来替代N和M
compare("hi", "mom");
注意:非类型模板参数的模板实参必须是常量表达式。
函数模板也可以声明为inline
或constexpr
的,inline
或constexpr
说明符要放在模板参数列表之后,返回类型之前:
//正确:inline说明符跟在模板参数列表之后
template<typename T> inline T min(const T&, const T&);
//错误:inline说明符的位置不正确
inline template<typename T> T min(const T&, const T&);
为了生成一个模板的实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包含声明也包括定义。
用来实例化模板的所有函数、类型以及与类型关联的运算符的声明都必须是可见的,这是由模板的用户来保证的。模板的设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。
保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。
类模板是用来生成类的蓝图的。与函数模板的不同之处是,编译器不能为类模板推断模板参数信息。为了使用类模板,必须在模板名后的尖括号中提供额外信息。
// forward declarations needed for friend declarations in Blob
template <typename> class BlobPtr;
template <typename> class Blob; // needed for parameters in operator==
template <typename T>
bool operator==(const Blob<T>&, const Blob<T>&);
template <typename T> class Blob {
// each instantiation of Blob grants access to the version of
// BlobPtr and the equality operator instantiated with the same type
friend class BlobPtr<T>;
friend bool operator==<T>
(const Blob<T>&, const Blob<T>&);
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
// constructors
Blob();
#ifdef INITIALIZER_LIST
Blob(std::initializer_list<T> il);
// if no initializer_list support use the iterator constructor
#endif
template <typename It> Blob(It b, It e);
Blob(T*, std::size_t);
// return BlobPtr to the first and one past the last elements
BlobPtr<T> begin() { return BlobPtr<T>(*this); }
BlobPtr<T> end()
{ auto ret = BlobPtr<T>(*this, data->size());
return ret; }
// number of elements in the Blob
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// add and remove elements
void push_back(const T &t) {data->push_back(t);}
void push_back(T &&t) { data->push_back(std::move(t)); }
void pop_back();
// element access
T& front();
T& back();
T& at(size_type);
const T& back() const;
const T& front() const;
const T& at(size_type) const;
T& operator[](size_type i);
const T& operator[](size_type i) const;
void swap(Blob &b) { data.swap(b.data); }
private:
std::shared_ptr<std::vector<T>> data;
// throws msg if data[i] isn't valid
void check(size_type i, const std::string &msg) const;
};
// constructors
template <typename T>
Blob<T>::Blob(T *p, std::size_t n):
data(std::make_shared<std::vector<T>>(p, p + n)) { }
template <typename T>
Blob<T>::Blob():
data(std::make_shared<std::vector<T>>()) { }
template <typename T> // type parameter for the class
template <typename It> // type parameter for the constructor
Blob<T>::Blob(It b, It e):
data(std::make_shared<std::vector<T>>(b, e)) { }
#ifdef INITIALIZER_LIST
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il):
data(std::make_shared<std::vector<T>>(il)) { }
#endif
// check member
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);
}
// element access members
template <typename T>
T& Blob<T>::front()
{
// if the vector is empty, check will throw
check(0, "front 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> void Blob<T>::pop_back()
{
check(0, "pop_back on empty Blob");
data->pop_back();
}
template <typename T>
const T& Blob<T>::front() const
{
check(0, "front on empty Blob");
return data->front();
}
template <typename T>
const T& Blob<T>::back() const
{
check(0, "back on empty Blob");
return data->back();
}
template <typename T>
T& Blob<T>::at(size_type i)
{
// if i is too big, check will throw, preventing access to a nonexistent element
check(i, "subscript out of range");
return (*data)[i]; // (*data) is the vector to which this object points
}
template <typename T>
const T&
Blob<T>::at(size_type i) const
{
check(i, "subscript out of range");
return (*data)[i];
}
template <typename T>
T& Blob<T>::operator[](size_type i)
{
// if i is too big, check will throw, preventing access to a nonexistent element
check(i, "subscript out of range");
return (*data)[i];
}
template <typename T>
const T&
Blob<T>::operator[](size_type i) const
{
check(i, "subscript out of range");
return (*data)[i];
}
// operators
template <typename T>
std::ostream&
operator<<(std::ostream &os, const Blob<T> a)
{
os << "< ";
for (size_t i = 0; i < a.size(); ++i)
os << a[i] << " ";
os << " >";
return os;
}
template <typename T>
bool
operator==(const Blob<T> lhs, const Blob<T> rhs)
{
if (rhs.size() != lhs.size())
return false;
for (size_t i = 0; i < lhs.size(); ++i) {
if (lhs[i] != rhs[i])
return false;
}
return true;
}
Blob<int> ia; //空Blob
Bloc<int> ia2 = {0, 1, 2, 3, 4}; //有5个元素的Blob
默认情况下,如果一个成员函数没有被使用,则它不会被实例化。成员函数只有当被用到时才进行实例化。
在大多数情况下,我们使用一个类模板类型必须提供模板实参,但这一规则有一个例外。在类模板自己的作用域中,可以直接使用模板名而不提供实参。例如在下面定义的BlobPtr
模板类中,前置递增和递减成员返回BlobPtr&
,而不是BlobPtr
。这是因为当处于一个类模板的作用域中时,编译器处理模板自身引用时就好像我们已经提供了与模板参数匹配的实参一样。
// BlobPtr throws an exception on attempts to access a nonexistent element
template <typename T>
bool operator==(const BlobPtr<T>&, const BlobPtr<T>&);
template <typename T> class BlobPtr : public std::iterator<std::bidirectional_iterator_tag,T> {
friend bool operator==<T>(const BlobPtr<T>&, const BlobPtr<T>&);
public:
BlobPtr(): curr(0) { }
BlobPtr(Blob<T> &a, size_t sz = 0):
wptr(a.data), curr(sz) { }
T &operator[](std::size_t i)
{ auto p = check(i, "subscript out of range");
return (*p)[i]; // (*p) is the vector to which this object points
}
const T &operator[](std::size_t i) const
{ auto p = check(i, "subscript out of range");
return (*p)[i]; // (*p) is the vector to which this object points
}
T& operator*() const
{ auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p) is the vector to which this object points
}
T* operator->() const
{ // delegate the real work to the dereference operator
return & this->operator*();
}
// increment and decrement
BlobPtr& operator++(); // prefix operators
BlobPtr& operator--();
BlobPtr operator++(int); // postfix operators
BlobPtr operator--(int);
private:
// check returns a shared_ptr to the vector if the check succeeds
std::shared_ptr<std::vector<T>>
check(std::size_t, const std::string&) const;
// store a weak_ptr, which means the underlying vector might be destroyed
std::weak_ptr<std::vector<T>> wptr;
std::size_t curr; // current position within the array
};
// equality operators
template <typename T>
bool operator==(const BlobPtr<T> &lhs, const BlobPtr<T> &rhs)
{
return lhs.wptr.lock().get() == rhs.wptr.lock().get() &&
lhs.curr == rhs.curr;
}
template <typename T>
bool operator!=(const BlobPtr<T> &lhs, const BlobPtr<T> &rhs)
{
return !(lhs == rhs);
}
// check member
template <typename T>
std::shared_ptr<std::vector<T>>
BlobPtr<T>::check(std::size_t i, const std::string &msg) const
{
auto ret = wptr.lock(); // is the vector still around?
if (!ret)
throw std::runtime_error("unbound BlobPtr");
if (i >= ret->size())
throw std::out_of_range(msg);
return ret; // otherwise, return a shared_ptr to the vector
}
// member operators
// postfix: increment/decrement the object but return the unchanged value
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
// no check needed here; the call to prefix increment will do the check
BlobPtr ret = *this; // save the current value
++*this; // advance one element; prefix ++ checks the increment
return ret; // return the saved state
}
template <typename T>
BlobPtr<T> BlobPtr<T>::operator--(int)
{
// no check needed here; the call to prefix decrement will do the check
BlobPtr ret = *this; // save the current value
--*this; // move backward one element; prefix -- checks the decrement
return ret; // return the saved state
}
// prefix: return a reference to the incremented/decremented object
template <typename T>
BlobPtr<T>& BlobPtr<T>::operator++()
{
// if curr already points past the end of the container, can't increment it
check(curr, "increment past end of BlobPtr");
++curr; // advance the current state
return *this;
}
template <typename T>
BlobPtr<T>& BlobPtr<T>::operator--()
{
// if curr is zero, decrementing it will yield an invalid subscript
--curr; // move the current state back one element
check(-1, "decrement past begin of BlobPtr");
return *this;
}
当一个类包含一个友元声明时,类与友元各自是否是模板是相互无关的。
template <typename T> calss pal;
template <typename T>
class C{
//C的每个实例将相同实例化的Pal声明为友元
friend class Pal<T>; //Pal的模板声明必须在作用域内
//Pal2的所有实例都是C的每个实例的友元,不需要前置声明
template <typename X> friend class Pal2; //为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数
//Pal3是一个非模板类,它是C的所有实例的友元
friend class Pal3; //不需要Pal3的前置声明
//将访问权限授予用来实例化C的类型
friend T;
}
可以通过using
来为类模板定义一个类型别名:
template<typename T> using twin = pair<T, T>;
twin<string> authors; //authors 是一个pair
//定义一个模板别名时,可以固定一个或多个模板参数
template<typename T> using partNo = pair<T, unsigned>;
partNo<string> books; //books是一个piar
类模板可以声明static
成员,例如对于一个名为Foo
的模板类,所有的Foo
类型共享相同的static
成员。
template<typename T> class Foo{
public:
static std::size_t count() {return ctr;}
private:
static std::size_t ctr;
};
template<typename T>
std::size_t Foo<T>::ctr = 0; //定义并初始化ctr
//三个对象共享相同的Foo::ctr 和 Foo::count()成员
Foo<int> fi, fi2, fi3;
在普通(非模板)代码中,编译器掌握类的定义,因此它知道通过作用域运算符访问的名字是类型还是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和一个默认函数实参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;
}
当模板被使用时才会进行实例化这一特性意味着,相同的实例可能出现在多个对象文件中。在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。可以通过显式实例化来避免这种开销:
extern template declaration; //实例化声明
template declaration; //实例化定义
//实例化声明与定义
extern template class Blob<string>; //声明
template int compare(const int&, const int&); //定义
当编译器遇到extern
模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern
就表示承诺在程序其他位置有该实例化的一个非extern
声明(定义)。对于一个给定的实例化版本,可能有多个extern
声明,但必须只有一个定义。
//Application.cc
//这些模板类型必须在程序其他位置进行实例化
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2;//实例化会出现在其他位置
//Blob及其接受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]);//实例化出现在其他位置
//templateBuild.cc
//实例化文件必须为每个在其他文件中声明为`extern`的类型和函数提供一个(非extern)的定义
template int compare(const int&, const int&);
template class Blob<string>;//实例化类模板的所有成员
当我们编译此应用程序时,必须将templateBuild.o
与Application.o
链接在一起。
对于函数模板,编译器利用调用中的函数实参来确定其模板参数。从函数实参来确定模板实参的过程被称为模板实参推断。
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const
转换及数组或函数到指针的转换。
template <typename T> T fobj(T, T); //实参被拷贝
template <typename T> T fref(const T&, const T&); //引用
string s1("a value");
fobj(s1, s2); //调用fobj(string , string)
fref(s1, s2);//调用fref(const string&, const string&)
int a[10], b[42];
fobj(a, b); //调用f(int *, int *);
fref(a, b); //错误:数组类型不匹配
如果函数参数类型不是模板参数,则对实参进行正常的类型转换。
template<typename T> ostream& print(ostream &os, const T &boj)
{
return os<<obj;
}
ofstream f("output");
print(f, 10); //使用print(ostream &os, const int);将f转换为ostream&
//尾置返回允许我们在参数列表之后声明返回类型
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
//处理序列
return *beg; //返回序列中一个元素的引用
}
//上面之所以要用尾置类型,是因为在编译器遇到参数列表前,beg都是不存在的
//解引用运算符返回一个左值,因此通过`decltype`推断的类型为`beg`表示的元素的类型的引用
解引用运算符返回一个左值,因此通过decltype
推断的类型为beg
表示的元素的类型的引用。如果我们希望返回的是一个元素的值而非引用,可以使用标准库的类型转换模板,这些模板定义在头文件type_traits
中。通过remove_reference
来获得引用的元素的类型。
template <typename It>
auto fcn(It beg, It end) ->
typename remove_reference<decltype(*beg)>::type
{
//处理序列
return *beg; //返回序列中一个元素的拷贝
}
通常我们不能将一个右值引用绑定到一个左值上。但是C++语言在正常绑定规则之外定义了两个例外规则,这两个规则是move
这种标准库设施正确工作的基础。
template <typename T> void f3(T&&);
f3(42);//实参是一个int类型的右值,模板参数T是int
当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&
)时,编译器推断模板类型参数为实参的左值引用类型。例如当我们调用f3(i)
(i
是一个int
型变量)时,编译器推断T
的类型为int&
,而非int
。这时候f3
的函数参数看起来是int&
的右值引用,这就构成了引用折叠。折叠规则如下:
X& &
、X& &&
和X&& &
都折叠成类型X&
X&& &&
折叠成X&&
注意:引用折叠只能应用于间接创建的引用的引用,如类型别名和模板参数。
这些规则导致了如下结果:
T&&
),则它可以被绑定到一个左值。且如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个(普通)左值引用参数(T&
)。右值引用通常用于两种情况:模板转发其实参或模板被重载。
使用右值引用的函数模板通常使用如下的方式来进行重载:
template <typename T> void f(T&&); //绑定到非const右值
template <typename T> void f(const T&);//绑定到左值或const右值
标准库move
函数是使用右值引用的模板的一个很好的例子,它是这样定义的:
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
move
的函数参数T&&
是一个指向模板类型参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配。
string s1("hi!"),s2;
s2 = std::move(string("bye!"));//正确:从一个右值移动数据
s2 = std::move(s1); //正确:但在赋值之后,s1的值是不确定的
虽然不能隐式地将一个左值转换为右值引用,但我们可以用static_cast
显式地将一个左值转换为一个右值引用。
某些函数需要将一个或多个实参连同类型不变地转发给其他函数,包括实参类型是否是const
的以及实参是左值还是右值。
//接受可调用对象和另外两个参数的模板
//对“翻转”的参数调用给定的可调用对象
//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;
}
f(41, i); //f改变了实参i
flip(f, j, 42); //通过flip1调用f不会改变j。因为j的值被拷贝到t1中,f中的引用参数被绑定到t1,而非j,从而岂不会影响j。
为了解决上述问题,将函数参数定义为T1 &&
和T2 &&
,通过引用折叠就可以保持翻转实参的左值/右值属性。
template<typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t2, t1);
}
但是上述方案仍然存在着另一个问题,它对于接受左值引用的函数工作的很好,但是不能用于接受右值引用参数的函数。例如:
void g(int &&i, int &j)
{
cout << i<< " " << j << endl;
}
flip2(g, i, 42); //错误:不能从一个左值实例化int&&
我们可以使用一个名为forward
的新标准库设置来传递flip2
的参数。forward
返回该显式实参类型的右值引用,即forward
的返回类型是T&&
。
template<typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
flip(g, i ,42); //i以int&类型传递给g, 42以int&&类型传递给g
一个可变参数模板就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。存在两种参数包:
//Args是一个模板参数包;rest是一个函数参数包
//Args表示零个或多个模板类型参数
//rest表示零个或多个函数参数
template <typename T, typename... Args>
void foo(const T&t, const Args& ... rest);
对于下面的调用:
int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d);
foo(s, 42, "hi");
foo(d, s);
foo("hi");
编译器会为foo
实例化出四个不同的版本:
void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);
当需要知道包中有多少个元素时,可以使用sizeof...
运算符。
template<typename ... Args> void g(Args ... args)
{
cout << sizeof ... (Args) << endl; //类型参数的数目
cout << sizeof ... (args) << endl; //函数参数的数目
}
可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
//用来终止递归并打印最后一个元素的函数
//此函数必须在可变参数版本的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)
{
os << t << ","; //打印第一个实参
return print(os, rest...); //递归调用,打印其他实参
}
一个常用的转发和可变参数模板如下:
//fun有零个或多个参数,每个参数都是一个模板参数类型的右值引用
template<typename... Args>
void fun(Args&&... args) //将Args拓展为一个右值引用的列表
{
//work的实参既拓展Args又拓展args
work(std::forward<Args>(args)...);
}
这里希望将fun
的所有实参转发给另一个名为work
的函数,假定由它完成函数的实际工作。由于fun
的参数是右值引用,因此我们可以传递给它任意类型的实参;使用std::forward
传递这些实参,它们的所有类型信息在调用work
时都会得到保持。
特例化的本质是实例化一个模板,而非重载它。因此,特例化不影响函数匹配。
template<typename T> int compare(const T&, const T&);
//compare的特殊版本,处理字符数组的指针
template<>
int compare(const char* const &p1, const char* const &p2)
{
return strcmp(p1, p2);
}
模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。
与函数模板不同,类模板的特例化不必为所有模板参数提供实参。一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。
//原始的、最通用的版本
template<class T> struct remove_reference{
typedef T type;
};
//部分特例化版本,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{typedef T type;};
template <class T> struct remove_reference<T&&> //右值引用
{typedef T type;}
可以只特例化特定成员函数而不是特例化整个模板。
template <typename T> struct Foo{
Foo(const T &t = T()) : mem(t) { }
void Bar {/*...*/}
T mem;
//Foo的其他成员
};
template<> //我们正在特例化一个模板
void Foo<int>::Bar() //我们正在特例化Foo的成员Bar
{
//进行应用于int的特例化处理
}
c++模板特化偏特化
#include
using namespace std;
template<typename T1,typename T2>
class Test{
public:
Test(T1 i,T2 j):a(i),b(j){cout<<"模板类"<<endl;}
private:
T1 a;
T2 b;
};
template<> //全特化,由于是全特化,参数都指定了,参数列表故为空。
class Test<int ,char>{
public:
Test(int i,char j):a(i),b(j){cout<<"全特化"<<endl;}
private:
int a;
int b;
};
template<typename T2> //由于只指定了一部分参数,剩下的未指定的需在参数列表中,否则报错。
class Test<char,T2>{
public:
Test(char i,T2 j):a(j),b(j){cout<<"个数偏特化"<<endl;}
private:
char a;
T2 b;
};
template<typename T1,typename T2> //这是范围上的偏特化
class Test<T1*,T2*>{
public:
Test(T1* i,T2* j):a(i),b(j){cout<<"指针偏特化"<<endl;}
private:
T1* a;
T2* b;
};
template<typename T1,typename T2>//同理这也是范围上的偏特化
class Test<T1 const,T2 const>{
public:
Test(T1 i,T2 j):a(i),b(j){cout<<"const偏特化"<<endl;}
private:
T1 a;
T2 b;
};
int main()
{
int a;
Test<double,double> t1(0.1,0.2);
Test<int,char> t2(1,'A');
Test<char,bool> t3('A',true);
Test<int*,int*> t4(&a,&a);
Test<const int,const int> t5(1,2);
return 0;
}
#include
using namespace std;
//模板函数
template<typename T1,typename T2>
void fun(T1 a,T2 b){
cout<<"模板函数"<<endl;
}
//全特化
template<>
void fun(int a,char b){
cout<<"全特化"<<endl;
}
//函数不存在偏特化,以下代码是错误的
/*
template
void fun(char a,T2 b){
cout<<"偏特化"<
int main()
{
int a=0;
char b='A';
fun(a,a);
fun(a,b);
return 0;
}