左值引用和右值引用/move()函数/移动语义/完美转发

在C++中,左值(lvalue)和右值(rvalue)是表达式的两种基本分类,它们决定了表达式的结果在内存中的位置和状态。左值通常指的是具有持久状态的对象,它们有明确的内存地址,可以被多次赋值。而右值通常是临时的、没有持久状态的值,它们通常没有内存地址,或者其内存地址在表达式结束后就变得无效。

C++11引入了右值引用(rvalue reference),用T&&表示,作为对左值引用(lvalue reference,用T&表示)的补充。这一特性极大地增强了C++的表达能力,特别是在资源管理和性能方面。

(1)左值引用

左值引用是C++98就有的特性,它允许我们为已存在的对象创建一个别名。左值引用必须被初始化为一个左值,即一个具有持久状态的对象。

int a = 10;
int& b = a; // b是a的左值引用

(2)右值引用

右值引用是C++11新增的特性,它允许我们为右值(即临时对象或即将被销毁的对象)创建一个引用。这样,我们就可以对右值进行更复杂的操作,比如移动语义(move semantics)。

int&& c = 20; // c是整数字面量20的右值引用(但这种情况不常见,通常用于函数参数或返回值)

std::string foo() {
    return std::string("Hello, World!"); // 返回的临时字符串是一个右值
}

std::string &&d = foo(); // d是foo()返回的临时字符串的右值引用

但请注意,直接绑定一个右值到右值引用(如int&& c = 20;)并不是右值引用的主要用途。右值引用的主要用途是作为函数参数(实现移动语义)和返回值(允许链式调用等)。

(3)move函数

std::move 的作用:

  • 将左值转换为右值,以触发移动语义

  • 不会真正“移动”数据,只是改变对象的属性

  • 用于触发移动构造和移动赋值,避免深拷贝,提高性能。

std::move 的底层原理

  • std::move() 只是 static_cast<T&&>,不会改变对象的生命周期。

  • std::move(a) 只是告诉编译器 a 变成右值,但不会修改 a 本身。

#include<vector>
class Vector
{
private:
	int x, y, z;
public:
	Vector(int x, int y, int z) :x(x), y(y), z(z) {}
};
int main()
{
	std::vector<Vector> vec;
	vec.push_back(Vector(1,2,3));//2
	Vector a(4,5,6);
	vec.push_back(a);//1
	vec.push_back(std::move(a));//2
}

 我们来看一下这段代码,第一个push_back里是一个临时变量还记得吗?临时变量都是右值,第二个push_back,因为a是个左值所以传入的参数是个左值,第三个push_back我们使用了move方法本质上我们希望他变成一个右值进而发生移动语义,就是一个偷的过程,而不是复制的过程,让我们进到源码里看看是什么情况.要记住move 它不进行任何移动.还要知道一件事:

        在运行期move是无作为的.

 
    _CONSTEXPR20_CONTAINER void push_back(const _Ty& _Val) { // insert element at end, provide strong guarantee
        emplace_back(_Val);
    }
 
    _CONSTEXPR20_CONTAINER void push_back(_Ty&& _Val) {
        // insert by moving into element at end, provide strong guarantee
        emplace_back(_STD move(_Val));
    }

补充:

c++14 新增 remove_reference_t (引用移除) 

std::remove_reference
其中:
std::remove_reference_t 实现:

template< class T >
using remove_reference_t = typename remove_reference<T>::type;

样例:

#include <iostream> // std::cout
#include <type_traits> // std::is_same
 
template<class T1, class T2>
void print_is_same() {
  std::cout << std::is_same<T1, T2>() << '\n';
}
 
int main() {
  std::cout << std::boolalpha;
 
  print_is_same<int, int>();
  print_is_same<int, int &>();
  print_is_same<int, int &&>();
 
  print_is_same<int, std::remove_reference<int>::type>();
  print_is_same<int, std::remove_reference<int &>::type>();
  print_is_same<int, std::remove_reference<int &&>::type>();
}

输出:
true
false
false
true
true
true

 我们看到了一个push_back的重载我们通过调试可以得知,第一个push_back调用的是源码的第二个,第二个push_back调用的是源码的第一个,第三个调用的是第二个(偷懒一下),要注意的是(_Ty&& _Val)它并不是一个万能引用,因为vector是一个类模板,(之后我会出博客讲到万能引用和引用叠加等等...)这里的TY就是type的意思就是参数的类型,会进行模板推导.第一个push_back的参数是一个左值引用的形式,第二个是右值引用的形式,第二个会触发一个移动语义,将原先的a的内存偷了过来。

        为了加深理解,我们看一下move的源码并且拿过来将代码变为下面这样,变成我们自己的move看看是否能运行成功。

#include<vector>
#include <type_traits>
// FUNCTION TEMPLATE move
template <class _Ty>
constexpr std::remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
	return static_cast<std::remove_reference_t<_Ty>&&>(_Arg);
}
class Vector
{
private:
	int x, y, z;
public:
	Vector(int x, int y, int z) :x(x), y(y), z(z) {}
};
int main()
{
	std::vector<Vector> vec;
	vec.push_back(Vector(1,2,3));
	Vector a(4,5,6);
	vec.push_back(a);
	vec.push_back(move(a));
}

我们可以看到我们将move搬过来实现一样可以运行成功,我们来看源码,_t是C++14之后将原来的type的形式全部都变成type reference的形式,remove_reference_t就是将这个函数木板的类别<_Ty>它的加引用的情况都给去掉了,无论是左值引用(&)还是右值引用(&&)都会移除掉,之后再用static_cast强转为右值引用的形式,那么我们能看出move就是将参数原来的修饰符全部都删掉,在强转为右值引用输出,就是这么简单,move没有干任何移动的过程,所以还是那句话:

        std::move 并不会进行任何移动

        真正的移动是要自己写的,发生在之后也就是这里

public:
    template <class... _Valty>
    _CONSTEXPR20_CONTAINER decltype(auto) emplace_back(_Valty&&... _Val) {
        // insert by perfectly forwarding into element at end, provide strong guarantee
        auto& _My_data   = _Mypair._Myval2;
        pointer& _Mylast = _My_data._Mylast;
        if (_Mylast != _My_data._Myend) {
            return _Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...);
        }
 
        _Ty& _Result = *_Emplace_reallocate(_Mylast, _STD forward<_Valty>(_Val)...);
#if _HAS_CXX17
        return _Result;
#else // ^^^ _HAS_CXX17 ^^^ // vvv !_HAS_CXX17 vvv
        (void) _Result;
#endif // _HAS_CXX17
    }

然而move的作用也就是强行转换成右值引用。

(4)移动语义(Move Semantics)

移动语义是C++11引入的一个概念,它允许资源的所有权从一个对象“移动”到另一个对象,而不是通过拷贝。移动语义通过右值引用移动构造函数来实现,从而提高了程序的性能。

4.1 为什么需要移动语义?

当我们处理大型对象(如 std::vectorstd::string 等)时,拷贝操作通常非常昂贵,因为这些对象可能会涉及大量的动态内存分配和数据复制。移动语义允许我们在这些对象之间转移所有权,而不是进行复制,从而节省时间和资源。

4.2 如何实现移动语义?

C++中的移动语义是通过右值引用和移动构造函数来实现的。右值引用允许我们识别临时对象,而移动构造函数则允许将对象的资源(如内存)从一个对象转移到另一个对象,而不需要拷贝数据。

移动构造函数的示例:

#include <iostream>
#include <vector>

class MyClass {
public:
    std::vector<int> data;

    MyClass() {
        std::cout << "Default constructor" << std::endl;
    }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept {
        std::cout << "Move constructor" << std::endl;
        data = std::move(other.data);  // 通过std::move转移资源 必须显示使用std::move,否则就是触发拷贝构造函数
        //data = other.data// 不行,会触发拷贝构造函数而不是移动构造函数
    }
};

int main() {
    MyClass obj1;                  // 默认构造
    MyClass obj2 = std::move(obj1); // 移动构造,obj1的资源被转移到obj2
}

解释:

  • MyClass(MyClass&& other) 是一个移动构造函数,它通过 std::moveother 对象的资源(data)转移给新的对象。
  • std::move(other.data) 并不会进行拷贝,而是“转移”资源。

在上面的代码中:

  • obj1 是原始对象,obj2 是通过移动构造从 obj1 创建的新对象。
  • 移动构造不会拷贝数据,而是将 obj1 的内部数据转移到 obj2,同时确保 obj1 不再拥有这些资源。

(5)完美转发(Perfect Forwarding)

在 C++11 之前,泛型函数在传递参数时无法保持参数的原始类型(左值或右值),导致额外的拷贝或移动操作。完美转发(Perfect Forwarding)是一种 高效传递参数 的技术,能够 保持参数的原始特性,避免额外的性能开销。

5.1 什么是完美转发?

完美转发 是指 在泛型模板函数中,以参数的原始形式(左值或右值)传递给目标函数,从而避免 不必要的拷贝或移动操作

通过完美转发,我们确保传递给另一个函数的参数类型和价值(左值或右值)保持一致。

5.2 如何实现完美转发?

完美转发依赖于 右值引用std::forwardstd::forward 是一个特殊的函数模板,它在传递参数时能够保留其原始的类型(左值或右值)。

示例代码:

#include  < iostream>
using namespace std;
void process(int &x){cout << "Lvalue reference: " << x << endl;}
void process(int &&x)  {cout << "Rvalue reference: " << x << endl; }

// 泛型函数,使用完美转发
template <typename T>
void forwardExample(T&& arg) 
{    
    process(std::forward<T>(arg));  // 关键:std::forward 保持原始类型
}
int main() 
{
    int a = 10;
    forwardExample(a);// 传递左值    
    forwardExample(20);  // 传递右值    
    return 0;
}

//输出
Lvalue reference: 10
Rvalue reference: 20

分析:

  • std::forward<T>(arg) 让 arg 保持左值或右值特性,从而 正确调用 process(int&) 或 process(int&&)

  • forwardExample(a) 传递左值,std::forward<T>(a) 仍是左值。

  • forwardExample(20) 传递右值,std::forward<T>(20) 仍是右值。

5.3 如果去掉 std::forward 会怎样?
template <typename T>
void forwardExample(T&& arg) 
{    
    process(arg);  // 没有 std::forward
}
//输出
Lvalue reference: 10
Lvalue reference: 20  // 右值变成了左值!

错误分析:

  • arg 在 process(arg) 语境中变成了左值,即使 forwardExample(20) 传递的是右值,arg 也会 丢失右值特性

  • 结果:process(int&&) 无法调用,所有右值都会被当成左值,导致 额外拷贝或移动

5.4 std::forward 的工作原理 

std::forward<T>(arg) 通过 引用折叠(Reference Collapsing)和 类型推导 来决定参数是否应该保留右值特性。

传入实参T 推导结果函数参数类型 (T&&)引用折叠后类型std::forward<T>(arg) 的类型值类别
int x(左值)int&int& &&int&int&左值
const int x(左值)const int&const int& &&const int&const int&左值
20(右值)intint&&int&&int&&右值
std::move(x)intint&&int&&int&&右值
T&& val = 左值引用实参(手动调用 wrapper)int&int& &&int&int&左值
T&& val = 右值引用实参(手动调用 wrapper)int&&int&& &&int&&int&&右值

核心规则:

  • T&& 绑定左值 时,T 会被推导为 int&,最终 std::forward<int&>(arg) 仍是左值。

  • T&& 绑定右值 时,T 会被推导为 int,最终 std::forward<int>(arg) 仍是右值。

5.5 完美转发的应用场景 

(1)传递构造函数参数

class Myclass {
public:
    template <typename T>
    Myclass(T&& arg) : data(std::forward<T>(arg)) {)
private:
    int data;
};

std::forward<T>(arg) 确保 arg 以最佳方式传递给 data,避免不必要的拷贝。

(2)传递函数参数 

#include <iostream>
#include <string>
#include <utility>

void print(const std::string& s) 
{
    std::cout << "Lvalue: " << s << std::endl;
}

void print(std::string&& s) 
{
    std::cout << "Rvalue: " << s << std::endl;
}

// 通过完美转发调用 print
template <typename T>
void callPrint(T&& arg) 
{
    print(std::forward<T>(arg));  // ✅ 注意是 std::forward
}

int main() {
    std::string str = "Hello";

    callPrint(str);            // 传左值 → 调用 Lvalue 版本
    callPrint("World");        // 传右值 → 调用 Rvalue 版本
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值