传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
左值引用和右值引用
在讲之前,我们先来看一下什么是左值和右值
左值和左值引用
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋
值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左
值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int main()
{// p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;// 以下几个是对上面左值的左值引用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;return 0;
}
右值和右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引
用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址。右值引用就是对右值的引用,给右值取别名。由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。通常情况下,我们只能是从右值表达式获得其引用。比如:
T && a = ReturnRvalue();
这个表达式中,假设ReturnRvalue
返回一个右值,我们就声明了一个名为a
的右值引用,其值等于ReturnRvalue
函数返回的临时变量的值。ReturnRvalue
函数返回的右值在表达式语句结束后,其生命也就终结了,而通过右值引用的声明,该右值又“重获新生”,其生命周期将于右值引用类型变量a的生命周期一样。只要a还“活着”,该右值临时量将会一直“存活”下去。
所以相比于以下语句的声明方式:
T b = ReturnRvalue();
我们刚才的右值引用变量声明,就会少一次对象的析构及一次对象的构造。因为a是右值引用,直接绑定了ReturnRvalue
返回的临时量,而b只是由临时值构造而成的,而临时量在表达式结束后会析构应而就会多一次析构和构造的开销。
double fmin(double x, double y)
{return x + y;
}int main()
{// 以下几个都是常见的右值double x = 1.1, y = 2.2; // 字母常量10; x + y; // 表达式返回值double ret = fmin(x, y); // 函数返回值// 以下几个都是对右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);// 这里编译会报错:error C2106: “=”: 左操作数必须为左值/*10 = 1;x + y = 1;fmin(x, y) = 1;*/return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。
左值引用与右值引用比较
先来回顾一下左值引用
int main()
{// 左值引用只能引用左值,不能引用右值。int a = 10;int& ra1 = a; // ra1是a的别名//int& ra2 = 10; // 编译失败,因为10是右值return 0;
}
我们思考一下下面2个问题:
左值引用可以引用右值吗?
右值引用可以引用左值吗?
通过上面的代码我们可以发现左值引用不能引用右值,那么为什么还要问这个问题了?其实我们可以引用右值,因为我们的测试还不够全面。可以看下面的情况:
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
常量左值
为什么加了const之后,就能引用右值了呢?这是因为字符常量具有常性,也就是不能被修改,如果我们使用引用来引用常量,那么我们就能通过引用来修改这个常量,这就违反了常量的特性。简单来说这是一个权限的放大,即“不能被修改变成了可以被修改”,所以加个const引用来限制。
在C++98标准中常量左值就是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。而且在使用右值对其初始化的时候,常量左值引用还可以像右值引用一样将右值的生命期延迟。不过相比于右值引用所引用的右值,常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。看下面的代码。
// 常量左值引用
struct Copyable
{Copyable(){}Copyable(const Copyable& o){cout << "Copied" << endl;}
};Copyable ReturnRvalue() { return Copyable(); }
void AcceptVal(Copyable){}
void AcceptRef(const Copyable &){}int main()
{cout << "Pass by value: " << endl;AcceptVal(ReturnRvalue()); // 临时值被拷贝传入cout << "Pass by reference: " << endl;AcceptRef(ReturnRvalue()); // 临时值被作为引用传递
}
我们声明了一个结构体Copyable
,该结构体的作用是在被拷贝到时候打印一句话Copied
。而两个函数AcceptVal
使用了值传递参数,而AcceptRef
使用了引用传递。在以ReturnRvalue
返回的右值为参数的时候,AcceptRef
就可以直接使用产生的临时值(并延长生命周期),而AcceptVal
则不能直接使用临时对象。
运行代码,结果如下:
Pass by value:
Copied
Copied
Pass by reference:
Copied
可以看到,由于使用了左值引用,临时对象被直接作为函数的参数,而不需要从中拷贝一次。在C++11中,同样的,可以使用以右值引用为参数声明如下函数void AcceptRvalueRef(Copyable &&) {}
也同样可以减少临时变量拷贝到开销。进一步地,还可以再AcceptRvalueRef
中修改该临时值(这个时候临时值由于被右值引用参数所引用,已经获得了函数时间的生命期)。不过修改一个临时值的意义通常不大。
在下表中,列出了在C++11中各种引用类型可以引用的值的类型。需要注意的是,只要能够绑定右值的引用类型,都能够延长右值的生命期。
[!note] 总结
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
接下来看右值引用
int main()
{// 右值引用只能右值,不能引用左值。int&& r1 = 10;// error C2440: “初始化”: 无法从“int”转换为“int &&”// message : 无法将左值绑定到右值引用int a = 10;int&& r2 = a; // 报错// 右值引用可以引用move以后的左值int&& r3 = std::move(a);return 0;
}
这里的move
函数是C++11之后新出的一个函数,其作用是将一个左值强制转换成一个右值,类似强制类型转换,还有move
并不会改变一个变量本身的左值属性,例如 int b = 1;double a = (double)b
这句代码,本质上b还是一个整型类型,只是在这个表达式中,返回了一个double类型的b。
[!note] 总结
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
右值引用的场景和意义
既然我们要知道右值引用的意义是什么,那么就需要先知道左值引用的优点和缺点是什么,有哪些场景要用到左值引用。
- 左值引用降低了内存使用和提高效率
string add_string(string& s1, string& s2)
{string s = s1 + s2;return s;
}int main()
{string str;string hello = "Hello";string world = "world";str = add_string(hello, world);return 0;
}
我们先来回顾一下非引用传参和引用传参的本质
- 非引用传参:在函数调用时,非引用传参实际上是传递了实参的一个副本给形参。这意味着在函数内部对形参的任何修改都不会影响到原始的实参。非引用传参包括普通形参和指针形参,但指针形参传递的是地址的副本,而不是对象本身的副本。
- 引用传参:引用传参则是将实参的引用(或别名)传递给形参。在函数内部对形参的操作实际上是对实参本身的操作,因此任何修改都会反映到原始的实参上。
所以引用传参不需要复制实参,而是直接操作实参本身,可以节省内存并提高效率。这对于大型对象或数据结构的传递尤为重要。而非引用传参需要复制整个对象或结构,这可能会导致较大的内存开销和较低的执行效率。
2. 左值引用解决了一部分返回值的拷贝问题
string& func()
{static string s = "hello world";return s;
}int main()
{string str1;str1 = func();return 0;
}
引用返回和非引用返回的区别:
- 非引用返回:当函数的返回类型为非引用类型时,函数的返回值用于初始化在调用函数时创建的临时对象。这意味着,如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象。其返回值既可以是局部对象(在函数内部定义的对象),也可以是求解表达式的结果。这种复制过程可能会导致额外的内存开销和性能损失。
- 引用返回:当函数返回类型为引用类型时,没有复制返回值。相反,返回的是对象本身(或更准确地说,是对对象的引用)。因此,返回引用类型通常更高效,因为它避免了不必要的复制操作。但是,需要注意的是,返回引用时必须确保引用的对象在函数返回后仍然有效。
从图中可以看出引用返回比非引用返回少了一次拷贝构造。这是因为返回值s指向的string是全局的,当出了函数作用域依然存在,因此我们可以传引用返回,不用拷贝构造给临时变量,节省了一次拷贝构造。
而在非引用返回函数当中,func函数依然返回hello world
这个字符串,但是s是一个局部变量。出了函数作用域就会被销毁,那么如果str1要想接收到s,那么就会创建一个临时变量拷贝构造给它,然后临时变量再拷贝构造给str。
那么如果我们不想使用引用返回,还想减少一次拷贝,该如何实现呢?答案就是使用右值引用。先来看看有哪些情况下会产生可以被右值引用的左值:
- 当一个左值被move后,可以被右值引用
- C++会把即将离开作用域的非引用类型的返回值当成右值,这种类型的右值也称为
将亡值
在C++11中,把右值分为纯右值和将亡值。
- 纯右值就是内置类型的右值,讲的是用于辨识临时变量和一些不跟对象关联的值。比如非引用返回的函数返回的临时变量值就是一个纯右值。一些运算表达式,比如
1+3
产生的临时变量值,也是纯右值。而不跟对象关联的字母量值,比如:2、‘c’;、true,也是纯右值。 - 将亡值就是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用
T&&
的函数返回值、std::move
的返回值或者转换为T&&
的类型转换函数的返回值。回顾刚才的代码,变量s已经快要离开作用域了,马上就会被销毁,s销毁没有问题,但是字符串hello world
是我们需要的。这种情况可以理解为:一个快要去世的病人,临走前说要把自己的器官捐赠给别人,当然也可以指定捐赠给他人。
同理,一旦左值得到了右值属性,相当于把自己的资源给别人,不希望自己的资源被系统释放,而是被合适的对象继承走。s
即将被销毁,此时s
就是一个右值了,右值的意思就是:这个变量的资源可以被迁移走。这句话非常非常重要!!!
<type_traits>头文件
有的时候,我们可能不知道一个类型是否是引用类型,以及是左值引用还是右值引用(在模板中比较常见)。标准库在<type_traits>
头文件中提供了3个模板类:is_rvalue_reference、is_lvalue_reference、is_reference
,可供我们进行判断。比如:
int main()
{cout << is_rvalue_reference<string&&>::value << endl; //1cout << is_rvalue_reference<string&>::value << endl; //0cout << is_lvalue_reference<string&>::value << endl; //1cout << is_reference<string&>::value << endl; //1
}
我们通过模板类的成员value
就可以打印出string &&
是否是一个右值引用了。配合类型推导符decltype
,我们甚至还可以对变量的 类型进行判断。
移动语义
那么右值是如何把资源迁移走的呢?这就要学习右值引用的移动语义
了:
拷贝构造函数中为指针成员分配新的内存再进行内容拷贝到做法在C++编程中几乎被视为是不可违背的。不过在一些时候,我们确实不需要这样的拷贝构造语义。我们先看下面的代码。
class HasPtrMem
{
public:HasPtrMem() :d(new int(0)){cout << "Construct: " << ++n_cstr << endl;}HasPtrMem(const HasPtrMem& h) :d(new int(*h.d)){cout << "Copy Construct: " << ++n_cptr << endl;}~HasPtrMem(){cout << "Destruct: " << ++n_dstr << endl;}int* d;static int n_cstr;static int n_dstr;static int n_cptr;
};int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;HasPtrMem GetTemp()
{return HasPtrMem();
}int main()
{HasPtrMem a = GetTemp();
}
在代码中,我们声明了一个返回一个HasPtrMem变量的函数。为了记录构造函数、拷贝构造函数,以及析构函数调用的次数,我们使用了一些静态变量。在main函数中,我们简单声明了一个HasPtrMem的变量a,要求它使用GetTemp的返回值进行初始化。运行结果如下(未开启编译器优化):
Construct: 1
Copy construct: 1
Destruct: 1
Copy construct: 2
Destruct: 2
Destruct: 3
首先在GetTemp函数中创建临时对象,调用HasPtrMem构造函数创建临时对象,输出Construct: 1
,接着,GetTemp
函数返回临时对象时进行拷贝构造,调用拷贝构造函数输出Copy construct: 1
,临时对象离开GetTemp
函数作用域,调用析构函数,输出Destruct: 1
。再然后main函数中进行拷贝构造,将 GetTemp()
函数返回的对象拷贝给对象 a
,调用拷贝构造函数,输出 Copy Construct: 2
。GetTemp()
函数返回的对象离开 main
函数中赋值语句的作用域,调用析构函数,输出 Destruct: 2
。最后main函数结束时析构对象a,输出Destruct: 3
。
如果开启了编译器优化,那么GetTemp
函数中创建的临时对象会直接作为对象a进行构造,不会发生拷贝构造。所以开启了编译器优化后,结果是:
接下来我以未开启优化来讲解。在代码中,类HasPtrMem
只有一个int类型指针。而如果HasPtrMem
的指针指向非常大的堆内存数据的话,那么拷贝构造的过程就会非常昂贵。可以想象,这种情况一旦发生,a的初始化表达式的执行速度将相当堪忧。
在main函数部分,按照C++的语义,临时对象将在语句结束后被析构,会释放它所包含的堆内存资源。而a在拷贝构造的时候,又会被分配堆内存。这样的一去一来似乎并没有太大的意义,那么我们是否可以在临时对象构造a的时候不分配内存,即不使用所谓的拷贝构造呢?
在C++11中,答案是肯定的,我们可以看下面的示意图:
上半部分可以看到从临时变量中拷贝构造变量a的做法,即在拷贝时分配新的堆内存,并从临时对象的堆内存中拷贝内容至a.d
。而构造完成后,临时对象将析构,因此其拥有的堆内存资源会被析构函数释放。而下半部分则是一种“新”方法,该方法在构造时使得a.d
指向临时对象的堆内存资源。同时我们保证临时对象对象不释放所指向的堆内存,那么在构造完成后,临时对象被析构,a就从中“偷”到了临时对象所拥有的堆内存资源。
在C++11中,这样的“偷走”临时变量中资源的构造函数,被称为“移动构造函数”。这样的“偷”的行为,则称为“移动语义”。可以理解为“移为己用”。通过下面的代码来看一看如何实现这样的移动语义。
class HasPtrMem
{
public:HasPtrMem() :d(new int(3)){cout << "Construct: " << ++n_cstr << endl;}HasPtrMem(const HasPtrMem& h) :d(new int(*h.d)){cout << "Copy Construct: " << ++n_cptr << endl;}HasPtrMem(HasPtrMem&& h) :d(h.d) // 移动构造函数{h.d = nullptr; // 将临时值的指针成员置为空cout << "Move construct: " << ++n_mvtr << endl;}~HasPtrMem(){delete d;cout << "Destruct: " << ++n_dstr << endl;}int* d;static int n_cstr;static int n_dstr;static int n_cptr;static int n_mvtr;
};int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;HasPtrMem GetTemp()
{HasPtrMem h;cout << "Resource from " << __func__ << ": " << hex << h.d << endl;return h;
}int main()
{HasPtrMem a = GetTemp();cout << "Resource from " << __func__ << ": " << hex << a.d << endl;
}
对比刚才的代码,这个代码多了一个构造函数HasPtrMem(HasPtrMem &&)
,这个就是移动构造函数。与拷贝构造函数不同的是,移动构造函数接受一个所谓的“右值引用”的参数。移动构造函数使用了参数h的成员d初始化了本对象的成员d(而不是像构造函数一样需要分配内存,然后将内容依次拷贝到新分配的内存中),而h的成员d随后被置为nullptr
。这样就完成了移动构造的全过程。
代码运行结果如下(未开启优化)
Construct: 1
Resource from GetTemp: 0x603010
Move construct: 1
Destruct: 2
Move construct: 2
Destruct: 2
Resource from main: 0x603010
Destruct: 3
可以看到,这里没有调用拷贝构造函数,而是调用了两次移动构造函数,移动构造函数的结果是,GetTemp
中的h的指针成员h.d
和main函数中的a的指针成员a.d的值是相同的,即h.d
和a.d
都指向了相同的堆地址内存。该堆内存在函数返回的过程中,成功的逃避了被析构的命运,取而代之地,成为了赋值表达式中的变量a的资源。如果堆内存不是一个int长度的数据,而是以mbyte
为单位的堆空间,那么这样的移动带来的性能提升是非常惊人的。
std::move()
在C++11中,标准库<utility>
中提供了一个有用的函数std::move
,这个函数的名字具有迷惑性,因为实际上move
并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而我们可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move
基本等同于一个类型转换:static_cast<T&&>(lvalue);
。
值得一提的是,被转化的左值,其生命期并没有随着左右值的转化而改变。来看下面的示例:
class Moveable
{
public:Moveable():i(new int(3)){}~Moveable() { delete i; }Moveable(const Moveable & m):i(new int(*m.i)){}Moveable(Moveable&& m) :i(m.i){m.i = nullptr;}int* i;
};int main()
{Moveable a;Moveable c(move(a)); // 会调用移动构造函数cout << *a.i << endl;
}
在代码中,我们为类型Moveable
定义了移动构造函数。这个函数定义本身没有什么问题,但是调用的时候,使用了Moveable c(move(a));
这样的语句。这里的a本来是一个左值变量,通过move后变成右值。这样一来,a.i
就被c的移动构造函数设置为指针空值。由于a的生命周期实际要到main
函数结束才结束,那么随后的表达式*a.i
进行计算的时候,就会发生严重的运行时错误。
来看一看正确的代码。
class HugeMem
{
public:HugeMem(int size) :sz(size > 0 ? size : 1) {c = new int[sz];}~HugeMem() { delete[]c; }HugeMem(HugeMem&& hm) :sz(hm.sz), c(hm.c) {hm.c = nullptr;}int* c;int sz;
};class Moveable
{
public:Moveable() :i(new int(3), h(1024) {}~Moveable() { delete i; }Moveable(Moveable && m) :i(m.i), h(move(m.h)) // 强制转化为右值,以调用移动构造函数{m.i = nullptr;}int* i;HugeMem h;
};Moveable GetTemp()
{Moveable tmp = Moveable();cout << hex << "Huge Mem from " << __func__ << " @" << tmp.h.c << endl;// Huge Mem from GetTemp @0x603030return tmp;
}int main()
{Moveable a(GetTemp());cout << hex << "Huge Mem from " << __func__ << " @" << a.h.c << endl;// Huge Mem from GetTemp @0x603030return 0;
}
在代码中,我们定义了两个类型:HugeMem
和Moveable
,其中Moveable
包含了一个HugeMem
的对象。在Moveable
的移动构造函数中,我们就看到了std::move
函数的使用。该函数将m.h
强制转化为右值,以迫使Moveable
中的h能够实现移动构造。这里也可以使用std::move
,是因为m.h是m的成员,既然m将存在表达式结束后被析构,其成员也自然会被析构,因此不存在上一个代码中生存期不对的问题。
那么如果不使用std::move(m.h)
这样的表达式,而是直接使用m.h
这个表达式会怎么样?这里的m.h
引用了一个确定的对象,而且m.h
也有名字,可以使用&m.h
取到地址,因此是一个不折不扣的左值。不过这个左值确实会很快“灰飞烟灭”,因为拷贝构造函数在Moveable
对象a的构造完成后也就结束了。那么这里使用std::move
强制转为右值就不会有问题了。而且,如果我们不这么做,由于m.h
是个左值,就会导致调用HugeMem
的拷贝构造函数来构造Moveable
的成员h。如果是这样,移动语义就没有能够成功地向类的成员传递。换言之,还是会由于拷贝而导致一定的性能上的损失。
事实上,为了保证移动语义的传递,在编写移动构造函数的时候,应该总是记得使用move
转换拥有形如堆内存、文件句柄等资源的成员为右值,这样一来,如果成员支持移动构造的话,就可以实现其移动语义。而即使成员没有移动构造函数,那么接受常量左值的构造函数版本也会轻松地实现拷贝构造,因此也不会引起大的问题。
完美转发
完美转发,是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。比如:
template <typename T>
void IamForwording(T t) {IrunCodeActually(t); }
这个例子中,IamForwording
是一个转发函数模板。而函数IrunCodeActually
则是真正执行代码的目标函数。对于目标函数IrunCodeActually
而言,它总是希望转发函数将参数按照传入Iamforwarding
时的类型传递(即传入的是左值对象,IrunCodeActually
就能获得左值对象,传入右值是也是一样),而不产生额外的开销,就好像转发者不存在一样。
这似乎是一件很容易的事情,但其实并不简单。在上面的例子中,在IamForwarding
的参数中使用了最基本类型进行转发,该方法会导致参数在传给IrunCodeActually
之前就产生了一次额外的临时对象拷贝。因此这样的转发只能说是正确的转发,但谈不上完美。
所以通常程序需要对是一个引用类型,引用类型不会产生额外的开销。其次,则需要考虑转发函数对类型的接收能力。因为目标函数可能需要既能够接受左值引用,又接受右值引用。那么如果转发函数只能接受其中的一部分,就无法做到完美转发。那么如果使用“万能”的常量左值类型呢?以常量左值为参数的转发函数会一些尴尬,比如:
void IrunCodeActually(int t) {}
template <typename T>
void IamForwording(const T& t) {IrunCodeActually(t); }
由于目标函数的参数类型是非常量左值类型,因此无法接受常量左值引用作为参数,这样一来,虽然转发函数的接受能力很高,但在目标函数的接受上却出了问题。那么我们可能就需要通过一些常量和非常量的重载来解决目标函数的接受问题。这在函数参数比较多的情况下,就会造成代码冗余,而且根据上面的表格中,如果我们的目标函数的参数是个右值引用的话,同样无法接受任何左值类型作为参数,间接的,也就导致无法使用移动语义。
那么C++11如何解决完美转发的问题的呢?实际上,C++11是通过“引用折叠”的新语言规则,并结合新的模板推导规则来完成完美转发。
在C++11以前,例如下面的语句:
typedef const int T;
typedef T& TR;
TR& v = 1; // 该声明在C++98中会导致编译错误
其中TR& v = 1
这样的表达式会被编译器认为是不合法的表达式,而在C++11中,一旦出现了这样的表达式,就会发生引用折叠,即将复杂的未知表达式折叠为已知的简单表达式,如下表。
TR的类型定义 | 声明v的类型 | v的实际类型 |
---|---|---|
T& | TR | A& |
T& | TR& | A& |
T& | TR&& | A& |
T&& | TR | A&& |
T&& | TR& | A& |
T&& | TR&& | A&& |
规则并不难记,因为一旦定义中出现了左值引用,引用折叠总是优先将其折叠为左值引用。而模板对类型的推导规则比较简单,当转发函数的实参是类型X的一个左值引用,那么目标参数被推导为X&
类型,而转发函数的实参是类型X的一个右值引用的话,那么模板的参数被推导为X&&
类型。结合以上的折叠规则,就能确定出参数的实际类型。进一步,我们可以把转发函数写成如下形式:
template <typename T>
void IamForwording(T&& t)
{IrunCodeActually(static_cast<T && > (t));
}
我们不仅在参数部分使用了T &&
这样的标识,在目标函数传参的强制类型转换中也使用了这样的形式。比如我们调用转发函数时传入了一个X类型的左值引用,可以想象,转发函数将被实例化为如下形式:
void IamForwording(X& && t)
{IrunCodeActually(static_cast<X& && > (t));
}
引用折叠规则就是:
void IamForwording(X& t)
{IrunCodeActually(static_cast<X&> (t));
}
对于一个右值而言,当它使用右值引用表达式引用的时候,该右值引用却是个左值,那么我们想在函数调用中继续传递右值,就需要使用move
来进行左右值的转换。而move
通常就是一个static_cast
。不过在C++11中,用于完美转发的函数却不再叫做move
,而是另外一个名字:forward
。所以我们可以把转发函数写成这样:
template <typename T>
void IamForwording(T && t)
{IrunCodeActually(forward(t));
}
move
和forward
在实际实现上差别并不大。来看一个完美转发的代码:
void RunCode(int&& m) { cout << "rvalue ref" << endl; }
void RunCode(int& m) { cout << "lvalue ref" << endl; }
void RunCode(const int&& m) { cout << "const rvalue ref" << endl; }
void RunCode(const int& m) { cout << "const lvalue ref" << endl; }template<typename T>
void PerfectForward(T&& t) { RunCode(forward<T>(t)); }int main()
{int a;int b;const int c = 1;const int d = 0;PerfectForward(a); // lvalue refPerfectForward(move(b)); // rvalue refPerfectForward(c); // const lvalue refPerfectForward(move(d)); // const rvalue ref
}
可以看到,所有的转发都被正确地送到了目的地。
完美转发的一个作用就是包装函数,这是一个很方便的功能,对上面的代码稍作修改,就可以用很少的代码记录单参数函数的参数传递状况。
template<typename T, typename U>
void PerfectForward(T&& t, U& Func)
{cout << t << "\tforwarded..." << endl;Func(forward<T>(t));
}void RunCode(double && m) {}
void RunHome(double && h) {}
void RunComp(double && c) {}int main()
{PerfectForward(1.5, RunComp); // 1.5 forwarded...PerfectForward(8, RunCode); // 8 forwarded...PerfectForward(1.5, RunHome); // 1.5 forwarded...
}