左值和右值
左值就是可以取地址的值,而右值包括两种:将亡值(使用move
转化而来)和临时变量。无论是将亡值还是临时变量,我们可以放心的使用移动语义来“窃取”它们的资源,从而避免了拷贝/析构的开销。
在讲到模板之前,我们先讨论一下,在普通的函数中,我们常使用的一种函数传参方法:
class A;
void foo(A a);
这种类型的传参采用的是值语义,会调用类A的构造函数和析构各一次,为了避免拷贝/析构的开销,故我们多采用引用的方法,如下:
void foo(A& a);
采用引用类型可以减少传参过程中的开销,但它只能用来传递非常量左值,且无法处理右值,也即对于const A
和字面值常量无法处理。故我们多采用指向常量的引用:
void foo(const A& a);
这种类型的形参不仅可以传递常量和非常量,还可以处理常量右值。这样说有点绕,我举个例子:
void foo(const string& str);
int main() {
string s("he");
const string cs("she");
foo(s);
foo(cs);
foo("world"); //字面值常量,也即常量右值
}
以上传参都是合法的。
如何处理未知的返回类型
在模板中,类模板需要显示的指定模板实参,而函数模板可以由编译器帮我们推导出参数类型。 例如对于函数sum
template <typename T>
T sum(T a, T b) {
return a + b;
}
int main() {
int i = 10;
float f = 12.9;
double d = 19.3;
sum(i, i); //这里实例化为sum(int, int);
sum(f, f); //实例化为sum(float, float);
sum(d, d); //实例化为sum(double, double);
}
编译器会根据我们传递的参数类型,在使用到的时候为我们生成相应的函数版本。当编译器遇到一个模板定义时,它并不生成代码,只有当我们实例化出一模板的一个特定版本时,编译器才会生成代码。为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。 对于上面的函数而言,当我们想要指定返回类型的时候,需要再定义一个形参:
template <typename T1, typename T2, typename T3>
T1 sum(T1 a, T2 b) {
return a + b;
}
这样的函数模板可以让不同类型的参数相加,同时返回一个指定的类型。但需要用户显示指定模板实参,参数类型在尖括号中给出。
int i = 10;
float f = 4.5;
auto d = sum<double>(i, f); //i和f由编译器推导出来,而返回值则在尖括号中指定
当我们希望用户确定返回类型时,用显式模板实参表示函数的返回类型是很有效的。但在其它情况下,会给用户带来负担。例如,我们编写一个函数,接收一对迭代器和返回序列中的一个引用。我们可以使用decltype
来推导出返回类型。
template <typename T>
auto foo(T beg, T end) -> decltype(*beg) {
//处理序列
return *beg;
}
如果想要返回值而不是引用的话,则需要使用标准库的类型转换模板。
template <typename T>
//这里使用typename关键字是告诉编译器这是一个类型成员,而不是一个静态数据成员
auto foo(T beg, T end) -> typename remove_reference<decltype(*beg)>::type
{
//处理序列
return *beg;
}
模板实参推断和引用
- 当一个函数参数是模板参数类型的一个普通左值引用时候,它只能绑定一个左值。
template <typename T>
void f1(T&);
f1(i); //i是int,则模板参数T是int
f1(ci); //i是const int,模板参数T是const int
f1(5); //错误,必须是一个左值
template <typename T>
void f2(const T&);
f2(i); //i是int,模板参数T是int
f2(ci); //ci是const int,模板参数T是int
f2(5); //指向常量的引用可以绑定一个右值,T是int
//能绑定右值其实是因为根据这个右值生成了一个临时变量
- 从右值引用函数参数推断类型
template <typename T>
void f3(T&&);
f3(5) //实参是一个int类型的右值,T是int
f3(i) //实参是int,模板参数T经过引用折叠后变为int&
f3(ci) //实参是const int,模板参数T经过引用折叠后变为const int&
可以发现:我们可以将任意类型的实参传递给T&&类型的函数参数。 如果一个函数参数是指向模板参数类型的右值引用(如,T&&),则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数将被实例化一个普通的左值引用(T&)。
template <typename T>
void f3(T&& val) {
T t = val; //是拷贝还是一个引用?
t = fcn(t); //赋值只改变t还是既改变t又改变val?
if(val == t) //若T是引用类型,则一直为true;
}
- 当我们对一个右值调用f3时,例如字面值常量42,T为int。在此情况下,局部变量t的类型为int,且通过拷贝参数val的值被初始化。当我们对t赋值时,参数val保持不变。
- 当我们对一个左值i调用f3时,则T为int&。当我们定义并初始化局部变量t时,赋予它类型int&。因此,对t的初始化将其绑定到val。当我们对t赋值时,也同时改变了val的值。在f3的这个实例化版本中,if判断永远得到true。
这种情况令人困惑。所以使用右值引用的函数模板通常使用以下两个函数重载。
template <typename T> void f(T&&); //绑定到非const右值;
template <typename T> void f(const T&); //绑定到const右值和左值
remove_reference
remove_reference是标准库提供了一个函数模板,它可以帮我们去除参数的引用。具体实现原理可以参考如下:
template <typename T>
class remove_reference
{
public:
typedef T type;
};
template<typename T>
class remove_reference<T&>
{
public:
typedef T type;
};
template<typename T>
class remove_reference<T&&>
{
public:
typedef T type;
}
move和forward
move是C++11表述库提供的一个模板函数,它可以将左值转化为右值。具体实现如下:
template <typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
具体原理不再赘述。而forward则可以在模板函数的参数传递之间,保持原有的参数类型。例如
template<typename T>
void process(const T& lval); // 传入的是左值,process以左值处理
template<typename T>
void process(T&& rval); // 传入的是右值,process以右值处理
template<typename T>
void fun(T&& param){
process(std::forward<T> (param));
}
我们希望经过process函数可以对传入的不同参数调用不同的版本。我们先不考虑forward函数,把它去掉。
fun(8);
//此时模板参数T为int,会调用左值引用版本
int i = 10;
fun(i);
//此时模板参数T为int&,依然调用左值引用版本
forward函数实现如下:
// FUNCTION TEMPLATE forward
template <class _Ty>
// forward an lvalue as either an lvalue or an rvalue
constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept {
return static_cast<_Ty&&>(_Arg);
}
template <class _Ty>
// forward an rvalue as an rvalue
constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept {
static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
return static_cast<_Ty&&>(_Arg);
}
forward函数右两个重载版本,当传入forward的参数为左值时,使用第一个版本,当传入的参数为右值时,使用第二个重载版本。
int i = 100;
fun(i);
传入一个左值,此时fun中T的类型为int&,param经过引用折叠后变为int&,在forward函数模板中,_Ty被推断出int&,。因此,在std::forward模板函数中,推断出_Ty的类型为int&,std::remove_reference用int& 进行实例化,std::remove_reference的type成员是int,在forward源码中有static_cast<int& &&>,则forward会返回左值引用类型
fun(9);
对于fun(9),传入的是一个右值, 那么fun中T的类型将是int,param类型是int&&。因此,在std::forward模板函数中,推断出_Ty的类型为int,std::remove_reference用int进行实例化,std::remove_reference的type成员是int,在forward源码中有static_cast<int&&>,则forward会返回右值引用类型