模板与泛型(二)
模板实参的推断和引用
函数模板的形参可以为左值引用或者是右值引用,如果左值引用,又可分为普通的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
为string
move
返回值类型为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
为string
move
返回值类型仍为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