c++-引用(包括完美转发,移动构造,万能引用)

📅 2026/7/23 19:35:58 👁️ 阅读次数 📝 编程学习
c++-引用(包括完美转发,移动构造,万能引用)

目录

左值和右值

左值引用和右值引用

引用与指针的区别

移动构造

完美转发

完美转发的例子

文章推荐

引用折叠、万能引用和完美转发那些事 - 殷大侠 - 博客园


左值和右值

在讨论引用之前,首先读者最好对左值和右值有一个清晰的了解和区分。

  1. 左值(lvalue)是指有内存地址、可被寻址,生命周期持久的表达式
  2. 右值(rvalue)是指临时值、不可被寻址、生命周期短暂的表达式
  3. 实际上,区分左值和右值的标准就是是否可以取地址,即该变量是否有身份

接下来举一些例子来清楚一下另两个概念:

int func() { int a = 10; return a; } static int b = 0; int& func2() { return b; } int main() { int a ;//a是局部变量,存放在栈中,可以寻址,拥有身份,a是左值。 int* p = &a;//p本质也就是存放地址的变量,p是左值。 *p; //*p指向的是a这块空间,而a是左值,因此*p是左值 int arr[5] = {0}; arr[0];//等价于*(arr+0),所以arr[0]也是左值 func2();//func2函数的返回值是一个空间的地址,存放在寄存器中,听起来他好像是右值,因为寄存器没有地址,但实际上因为func()的类型是引用类型会对地址自动解引用,实际上返回的是所引用的空间也就是b所在的控件,因此是左值。 100;//100存放在代码段某块空间的数据,但是100是伴随指令存在的,不是自己单独享有空间,因此实际上它没有空间,因此它是右值 "ssssss"//常量字符串存放在只读数据区,因此他是有地址的,是一个左值,但他不能修改 a * a;//a*a的结果存放在寄存器或者一块临时空间中,a*a是临时空间的引用,所以a*a是右值 func();//func()的返回值类型是值类型,返回值存放在寄存器,没有地址,因此是右值 }

接下来为读者介绍一下左右值的注意点:

  1. 左值不一定在左边(int a=b),右值一定在右边
  2. 左值和右值都是表示数据的表达式,即他们不单单是一个变量名,或者一个常量,左值和右值也可以是解引用的指针(*p),函数调用(func()),也可以是一个计算表达式(3+5,右值才有)

左值引用和右值引用

左值引用就是给左值取别名(&),右值引用就是给右值取别名(&&)。“别名”,从这个称呼也可以看得出来,引用没有开辟新空间,只是把空间或者空间中的数据再次引用一次(再取个名字)。在下面的例子中,x和a指向同一块空间,y和5指向同一块数据:

int a = 5; int& x = a;//左值引用 int&& y = 5;//右值引用

下面是对引用的补充

  • 右值引用可以延长右值的生命周期,比如将函数返回值用右值引用引用起来,返回值就不会立即销毁,但是左值引用不可以延长左值的生命周期
  • 左值引用加const可以给右值取别名(const int& x = 5)。右值引用也可以给左值取别名,但需要用move函数(int&& a= move(b)),这点我们接下来会讲。
  • 左值的引用也是左值。但是右值的引用却是左值。左值不必说,右值的引用为什么是左值,因为毕竟右值被引用后可以长期存在,有了自己的身份
    当然,也可以看作
    是编译器给的特例,也叫退化。如果不给这个特例的话,那么右值匹配到移动构造后,参数也是右值,右值无法修改,资源也就无法交换了,移动构造无法实现了,右值引用也就失去了存在的意义。这点我在下面也会讲
  • const,引用,右值引用不能引用左值,甚至右值,左值,这些都是语法层的概念,实际上底层没有这么多约束,可以一定程度绕过编译器的检查,例如:int&&a = (int &&)b(右值引用引用了b这个左值,move的原理实际上就是这个)。
  • 实际上,底层上没有别名的概念,别名就是一个指针,给一块空间取别名就是用另一个指针指向这块空间,只不过在于语法层上我们不会发觉,类似于这样:
int x = 5; int* p = &x;//p是x的引用,等价于int&p = x *p = 5;//等价于p = 5 //当然,我们定义引用时不需要使用指针,访问引用时也不需要*,这是编译器帮我们处理的

引用与指针的区别

  • 有多级指针,但是没有多级引用(可以引用一个引用,但这不是多级引用,因为他们引用的是一个空间)
  • 没有NULL引用,但有NULL指针
  • 引用在定义时必须初始化,指针没有要求
  • 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

移动构造

以右值引用为参数类型的拷贝构造函数就是移动构造函数,而通过移动构造函数构造对象的过程就叫做移动构造,见一下移动构造:

class A { public: A(A&& x) { //... } };

移动构造有什么用

  • 拷贝一个临时对象(右值)时自动匹配移动构造函数而不是普通拷贝构造函数(尽管普通拷贝构造函数也可以接受临时对象(加const),但普通拷贝不会交换,这是为了尽可能使用移动构造,提高效率)
  • 移动构造内部直接把引用的临时对象的资源与新对象的资源交换,不发生拷贝,只发生一次交换,提高了效率这也是为什么一个右值的右值引用是左值,不是左值就无法修改,也就无法交换对象资源。

举一个移动构造交换资源的例子

class A { public: A(int x) { _ptr = new int[x]; for (int i = 0; i < x; i++) { _ptr[i] = i; } } A(A&& x) { swap(this->_ptr,x._ptr);//交换资源,而不是拷贝 } void test() { for(int i = 0;i<5;i++) { cout << _ptr[i] << endl; } } private: int* _ptr; }; int main() { A a2(A(5));//调用移动构造 a2.test(); }

在上面的例子中a2对象通过交换获得了临时对象A(5)的资源。

移动构造在优化方面的作用

  • 一般来说,使用函数返回值构造新对象的过程是:局部对象-->拷贝-->临时对象--拷贝-->新的对象
  • 编译器优化:局部对象--拷贝-->新的对象(省去临时对象,直接拷贝)
  • 移动构造优化:局部对象--交换-->新的对象(在编译器优化的情况下把仅剩一次的拷贝 变成移动构造资源交换,自此,整个过程没有拷贝)

总的优化如下:

细心的同学可能发现,str不是右值,为什么还可以调用移动构造?实际上,像str这种,函数内局部变量,并且马上就要被销毁对象,叫做将亡值,也属于右值的一种(你都要去世了,还不如把资源给我~)。


完美转发

但是给了特例后也出现了一个问题:就是如果需要调用多层函数完成移动构造的话,右值经过参数传递会退化成左值,因此下层的函数无论如何也不会匹配到移动语义的函数而是拷贝语义最经典的就是容器中的emplace_back函数,这个函数会先接受一个参数然后在调用定位new的时候把参数传给要构造的对象的构造函数,但此时参数却退化成了左值,就算一开始传入右值也永远无法匹配移动构造,下面介绍的完美转发会解决这个问题

引用折叠

总之,只有折叠双方都是右值引用类型,结果才是右值引用,否则就是左值引用

万能引用

template<class T> void func(T && arg){}

如果函数的参数是T&&这种类型,且T的类型需要推导,那么这就是一种万能引用(也叫引用折叠)

  • 如果传入左值(假如是int类型),那么T的类型就是int &。根据引用折叠规则,fun函数实例化为func(T &arg)
  • 如果传入右值(假如是int类型),那么T的类型就是int 。根据引用折叠规则,fun函数实例化为func(T &&arg)

forward函数

template <typename T> T&& forward(typename remove_reference<T>::type& arg) { return static_cast<T&&>(arg); }

在这个模版函数中,arg就是万能引用的形参,T是万能引用的模版参数类型,这个函数返回一个左值或者右值,如果arg原来是左值,返回arg的左值引用,如果arg原来是右值(经过传参退化成了左值),返回arg的右值引用

forwad函数利用了万能引用推导出的T的类型,根据这个类型再使用引用折叠规则,把arg强转成左值或者右值(在c++中,把一个数强转成int&&类型,这个数就变成了右值,如果强转为左值,这个数就变成了左值)

总结下来,万能引用其实有两个作用:

  • 一个就是帮我们实现拷贝版本和移动版本的函数而不用自己手动写(万能引用)
  • 另一个就是保持所传参数的左右值属性(forward+引用折叠)

完美转发的例子

void func2(int&& x) { cout << "void func2(int&& x)" << endl; } } void func2(int & x) { cout << " func2(int & x)" << endl; } template<class T> void func(T&& x)//万能引用 { func2(forward<T>(x));//完美转发 } int main() { func(5); int a = 5; func(a); }

从上述例子我们可以看出:

  1. func()传入左值(a)时,forward函数不会改变他原有的属性,而是将参数再次传入func2(),所以调用func2时,调用到的是void func2(int & x)。
  2. func传入右值,但是又退化成了左值时,forward函数返回一个参数的右值引用,所以func2接收到的参数是右值,所以匹配到的是void func2(int&& x)。

事实上,如果你确定你调用func要传送的始终是右值,内部继续调用func2函数的时候想要保持参数的右值属性,还可以直接使用move(arg)函数,这个函数可以直接arg强转成右值然后返回:

void func2(int&& x) { cout << "void func2(int&& x)" << endl; } void func2(int & x) { cout << " func2(int & x)" << endl; } template<class T> void func(T&& x)//万能引用 { func2(move(x));//完美转发 } int main() { func(5); int a = 10; func(a); }

但是move可不管你原来是左值还是右值,到了人家哪儿,你必须变成右值。

文章推荐

引用折叠、万能引用和完美转发那些事 - 殷大侠 - 博客园