模板与泛型(二)
模板实参的推断和引用
函数模板的形参可以为左值引用或者是右值引用,如果左值引用,又可分为普通的T&和const T&,其参数推断规则如下
左值引用参数推断
当一个函数参数是一个普通的左值引用时T&,其传递的实参只能是一个变量或者是一个返回引用类型的表达式。实参可以是const类型也可以不是,如果是const的,则T被推断为const类型
template<typename T>
void f1(T& op); //实参必须是一个左值
f1(i); //i是一个int;T是int
f1(ci); //ci是一个const int,则T是const int
f1(5); //错误,5是一个右值
如果模板参数是const T&,则它可以接受任何类型的实参(const或非const的对象,临时变量和字面常量)。当实参是const类型时,T不会被推断为const类型,因为const已经是模板参数类型的一部分了
template<typename T>
void f2(const T& op); //可以接受右值
//f2的模板参数是const T&, 实参的const是无效的
//在下面每个调用中,f2的参数都被推断为const int&
f1(i); //i是一个int;T是int
f1(ci); //ci是一个const int,则T是int
f1(5); //const T&可以绑定右值,T是int
右值引用参数推断和引用折叠
同理当一个模板参数声明为右值引用时,它将接受一个右值作为实参
template<typename T>
void f3(T&&);
f3(42); //实参是一个int型右值,T是int
C++编译器对T&&做了一些特殊的设定,具体来说有两点,第一点是如果实参是一个左值,按照上面定义,它是不能直接绑定到右值引用(T&&)上的,但实际上却可以。假定i是一个int对象,当我们调用f3(i)时,编译器会推断T的类型为int&,而非int,此时f3接受的参数变成了一个左值引用的右值引用。通常,我们是不能定义一个引用的引用的,但是编译器为我们提供了第二条特殊设定,即如果我们间接创建了一个引用的引用,那么这些引用形成了折叠。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用。在新标准中,折叠也可产生一个右值引用,这种情况只能发生在右值引用的引用。即,对于一个给定类型X:
X& &,X& &&和X&& &都折叠成X&X&& &&折叠成X&&
这两个规则导致下面几个的结论,对于template<typename T> void f3(T&&)这样的模板
- 如果传递的参数是左值,
T推导的类型为T&,而T& &&会被折叠为T&,因此参数的实际类型为T& - 如果传递的参数时左值引用,情况和上面一致
- 如果传递的参数是右值,
T推导的类型为T,因此参数的实际类型为T&& - 如果传递的参数是右值引用,
T推导的类型为T&&,而T&& &&折叠成了T&&,因此参数的实际类型为T&&
T&& 的作用主要是保持值类别进行转发,它有个名字就叫“转发引用”(forwarding reference)。因为既可以是左值引用,也可以是右值引用,它也曾经被叫做“万能引用”(universal reference)。
理解std::move
我们可以通过std::move将一个左值绑定要一个右值引用上。由于move本质上可以接受任何类型的实参,因此推断它的实参为T&&
template<class T>
constexpr remove_reference_t<T>::type&& move(T&& t) noexcept {
return static_cast<typename remove_reference_t<T>::type&&>(t)
}
通过引用折叠,move即可以接受左值,也可以接受右值。我们分析两个例子
string s1("hi!");
string s2 = std::move(string("bye!"));
对于s1
- 类型推断
T为string remove_reference用string进行实例化,remove_reference<string>::type为stringmove返回值类型为string&&,move的参数类型为string&&
因此,move被实例化为
string&& move(string&& t) {
return static_cast<string&&>(t);
}
由于t自身就是string&&,因此static_cast什么都不做,直接返回t。
对于s2,std::move接收的是一个左值引用string&
- 类型推断
T为string& remove_reference用string&进行实例化,remove_reference<string&>::type为stringmove返回值类型仍为string&&move的参数t实例化为string& &&,根据上面的规则,会折叠为string&
因此,move被实例化为
string&& move(string& t) {
return static_cast<string&&>(t);
}
由于此时t是string&,因此static_cast会将其cast成string&&。实际上,C++允许使用static_cast来转换一个左值到右值,例如下面代码是合法的
int a = 10;
int&& b = static_cast<int&&>(a);
转发
某些函数需要将一个或者多个实参类型不变的转发给其它函数,不论是否是const,左值还是右值。C++提供了一个std::forward的函数来转发模板类型的参数,std::forward可以保持给定实参的左值/右值属性
template<typename F, typename T1, typename T2>
void flip(F f, T1&& t1, T2&& t2) {
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
使用std::forward有两点需要注意
- 传递的参数通常为
T&&,即可以绑定到任意参数 - 与
std::move不同,std::forward<T>必须通过显式模板实参来调用
可变参数模板
可变参数模板接受任意多个模板参数和函数参数,用typename...或者classname...表示。例如
// Args表示一个或多个模板参数
// rest表示一个或多个是函数参数
template<typename T, typename... Args>
void foo(const T& t, const Args& ... rest);
编译器会根据实参实例化出不同的模板,例如
int i=0; double d = 3.14; string s = "hi";
foo(i, s, 42, d);
foo(s, 32, "hi");
foo(d, s);
foo("hi");
编译器会为foo实例化出四个不同版本
void foo(cosnt int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[2]&);
void foo(const double& const int&);
void foo(const char[2]&);
当我们既不知道函数的参数个数,也不知道它们的类型时,可变参数模板是很有用的。另一个例子是可以用可变参数模板来处理递归
template<typename T>
ostream &print(ostream &os, const T& t) {
return os<<t; //print t
}
template<typename T, typename...Args>
ostream &print(ostream &os, const T& t, const Args&... rest) {
os<< t << ",";
return print(os, rest...); //递归
}
print(cout, i, s, 42);
此时前两次调用会走可变参数函数,最后一次调用走第一个普通模板函数
sizeof...
sizeof...返回一个常量表达式,可以用来知道模板和函数有多少个参数
template<typename ... Args>
void g(Args ... args) {
std::cout<<sizeof...(Args)<<std::endl;
std::cout<<sizeof...(args)<<std::endl;
}
std::forward
我们可以用std::forward将可变参数原封不动的传给其它函数
class StrVec {
public:
temaplate<class ... Args>
void emplace_back(Args&&...) {
// some code
alloc.construct(first_free++, std::forward<Args>(args)...);
}
}
前面提到过,std::forward接受的参数通常是T&&,对于可变参数也是如此。上面的emplace_back接受一个string变量,由于string有多个构造函数,且参数都不相同,因此emplace_back接受的是可变参数,例如
svec.emplace_back(10, 'c'); // add cccccccccc to the end of the array
编译器会产生下面参数传给cosntruct
std::forward<int>(10), std::forward<char>(c)
类似的,如果传入一个右值,svec.emplace_back("hi");,它将以如下形式传递 std::forward<string>(string("hi"))
类模板的特例化
前面文章中提到函数模板可以特例化,这里我们继续讨论类模板的特例化,一个好的例子是hash模板,因为不同的类的hash运算不一样,因此我们需要为每个类实现一个特例化的hash函数。一个特例化的hash类必须定义
- 一个重载的调用运算符
operator()返回类型为size_t,参数类型为T - 定义两个类型成员(type members)作为
operator()的 - 定义默认构造函数和拷贝赋值运算
我们需要在namespace std下面特例化这个模板
namespace std {
template<>
struct hash<Sales_data>{
typedef size_t result_type;
typedef Sales_data argument_type;
size_t operator()(const Sales_data &) const;
};
size_t hash<Sales_data>::operator()(const Sales_data &) const {
return hash<string>()(s.bookNo) ^
hash<string>()(s.units_sold) ^
hash<string>()(s.revenue);
}
}
由于hash<Sale_data>使用Sale_data的私有成员,我们必须将其声明为Sales_data的友元
class Sale_data {
//...
template<class T> class std::hash;
friend class std::hash<Sales_data>
//...
};
与函数模板不同,类模板的参数可以部分特例化,例如remove_reference的定义如下
template<class T> struct remove_reference {
typedef T type;
};
// partially specialized for lvalue and rvalue reference
template<class T> struct remove_reference<T&> {
typedef T type;
};
template<class T> struct remove_reference<T&&> {
typedef T type;
}
部分特例化的模板参数必须是原模板参数的一个子集,上面例子中T&和T&&都是T的一个特例。下面例子中我们定义一个int型的a
//decltype(42) is int, use the original template
remove_reference<decltype(42)>::type a;
int i;
//decltype(i) is int&, use the T& template
remove_reference<decltype(i)>::type a;
//decltype(std::move(i)) is int&&, use the T&& template
remove_reference<decltype(std::move(i))>::type a;
我们还可以特例化类的成员函数
template<typename T>
struct Foo {
Foo(const T& = T()):mem(t) {}
void Bar() { ... }
T mem;
};
template<>
void Foo<int>::Bar() {
//Foo<int> is specialized for int
}
//main.cpp
Foo<int> fi;
fi.Bar() ; //this will call the specilized function