【C++11】右值引用+移动语义+完美转发

右值引用+移动语义+完美转发

右值引用、移动语义和完美转发

github地址

有梦想的电信狗

前言

在传统的 C++ 中,引用机制主要用于避免拷贝开销和提高效率,但自 C++11 起,语言引入了右值引用移动语义完美转发这三项关键特性,让资源管理与性能优化进入了新的阶段。

这些新特性解决了旧语法中"临时对象反复拷贝""容器插入效率低""泛型函数无法正确转发右值"等问题,使得对象资源能够安全地被移动、被复用、被完美传递

本文将系统讲解:

  • 左值与右值的本质区别
  • 右值引用的设计动机与语法规则
  • 移动语义在性能优化中的应用
  • 完美转发std::forward 的工作机制。

通过配图、示例与实测分析,读者将从语法到底层彻底掌握这一组 C++11 的核心机制,为后续学习 STL 容器的现代实现、泛型编程与高性能系统代码打下坚实基础。


一、左值和右值

1. 左值

  • 左值 :是一个表示数据的表达式(如变量名解引用的指针 ),一般以持久状态存储在内存中。

    • 左值一定可以取地址,一般可以赋值。
    • 定义时被 const 修饰的左值定义之后不能再给它赋值,但可以取它的地址。
  • 总结左值 (lvalue) :有名字、可寻址、能持久存在的对象。

    👉 可以出现在 = 的左边,也可以出现在 = 的右边

cpp 复制代码
// a, b, c, d  都是左值
int a = 10;
int b = a;
const int c = 30;	// 被 const 修饰的左值  依然是左值
const int* d = &c;

// 以下的 p, q, x, *p 都是左值
int* p = new int(10);
int q = 1;
const int x = 2;

// 以下也都是左值
"xxxxxx";  		// 该表达式的含义是 该字符串首元素的地址
const char* px = "xxxxxx"; 		// 该表达式的含义是,把 "xxxxxx" 首元素的地址赋值给 px
p[2];     // 左值

// 左值可以被取地址
cout << &("xxxxx") << endl;		// 可以取地址, 左值
cout << &px[2] << endl;			// 可以取地址, 左值

2. 右值

  • 右值 :也是一个表示数据的表达式,将亡值 为一种典型的右值
    • 常见右值举例 :字面常量(10, 1.1),**函数的传值返回值(**不能是左值引用返回),表达式求值过程中创建的临时对象匿名对象等。
    • 右值不能取地址右值也不能修改
  • 总结右值 (rvalue) :没有名字、不能持久存在的临时值。
    👉 一般出现在 = 的右边,不能出现在 = 的左边
    • 内置类型的右值 被称为纯右值
    • 自定义类型的右值 被称为将亡值

常见左值右值分类举例

表达式 左值还是右值 说明
int a = 10; a 是左值 a 有名字,可寻址
10 右值 字面量常量,临时值
a + 1 右值 表达式结果是临时对象
a = 5 左值 (注意!)赋值表达式的 结果本身 是左值
std::move(a) 右值 强制把左值转换成右值
*p 左值 解引用后得到可取地址的对象
a++ 右值 后置自增返回的是旧值的拷贝(临时)
++a 左值 前置自增返回的是自身

返回右值的函数fmin

cpp 复制代码
// 返回右值的函数
int fmin(int a, int b) {
	return a < b ? a : b;
}
void test2() {
	double x = 1.1, y = 2.2;
	// 以下三个表达式的返回值都是常见的右值
	10;
	x + y;			// 表达式的返回值为右值
	fmin(x, y);		// 这里的右值指的 是 函数返回值 中的 传值返回,返回的那个临时对象
	
    // 右值不能取地址
	/*cout << &10 << endl;
	cout << &(x + y) << endl;
	cout << &fmin(1, 2) << endl;*/
}
  • 值得一提的是,左值 的英文简写为lvalue右值 的英文简写为rvalue
    • 传统 认为它们分别是left valueright value 的缩写。
  • 现代C++中
    • lvalue 被解释为 loactor value 的缩写,可意为存储在内存中、有明确存储地址、可以取地址的对象
    • rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址的对象
      • 例如:临时变量,字面量常量,存储于寄存器中的变量等。
  • 左值和右值的核心区别是能否取地址


二、左值引用和右值引用

引用是给对象取别名

  • 左值引用 : 给左值取别名
  • 右值引用 : 给右值取别名

1. 左值引用 vs 右值引用

左值引用和右值引用的语法

  • 左值引用Type&

  • 右值引用Type&&

cpp 复制代码
double x = 1.5, y = 2.5;

// 左值引用: 给左值取别名
int a = 10;
int& r1 = a;

// 返回右值的函数
double fmin(double a, double b) {
	return a < b ? a : b;
}

// 右值引用: 给右值取别名
int&& r5 = 20;
//double& r6 = x + y;	// 编译不通过  不能用左值引用给右值取别名

double&& r6 = x + y;    // X + y 返回的结果为右值  右值引用
double&& r7 = fmin(x, y)	// 对fmin返回的右值 进行右值引用

2. const 左值引用给右值取别名

左值引用能否给右值取别名?

  • 左值引用不能直接给右值取别名const 左值引用 可以 给右值取别名
cpp 复制代码
double x = 1.5, y = 2.5;

// 普通左值引用: 给左值取别名
int a = 10;
int& r1 = a;

// 左值引用能否给右值取别名?
// const 左值引用 可以 给右值取别名
const int& r2 = 20;
const double r3 = x + y;

3. 右值引用不能直接给左值取别名

右值引用能否给左值取别名?

  • 右值引用不能直接给左值取别名 ,但可以给 std::move() 后的左值取别名
cpp 复制代码
// 普通右值引用: 给右值取别名
int&& r5 = 20;
double&& r6 = x + y;    // X + y 返回的结果为右值  右值引用

// 右值引用 可以给 std::move 后的左值取别名
//int&& r7 = a;	// 编译报错, 右值引用不能直接给左值取别名
int&& r7 = std::move(a);	// 可以给 std::move 后的左值取别名

4. std::move()的作用

std::move 是标准库的函数模板,定义简化如下:

cpp 复制代码
template <class T> 
typename remove_reference<T>::type&& move(T&& arg) 
{
    // 强制类型转换:将 arg 转为右值引用返回
    return static_cast<typename remove_reference<T>::type&&> (arg);
}
  • std::move 的本质是强制类型转换返回左值的右值引用std::move 后的左值可以绑定到右值引用
  • 注意std::move 本身不移动数据,只是 "允许右值引用绑定",真正的移动语义由移动构造 / 移动赋值 函数实现

5. 总结

  • 左值和右值本质区别 是:能否取地址
    • 内置类型的右值 被称为纯右值
    • 自定义类型的右值 被称为将亡值
  • 左值引用 : 给左值取别名,左值引用可以直接给左值取别名
    • const左值引用 可以 给右值取别名
  • 右值引用 : 给右值取别名,右值引用可以直接给右值取别名
    • 右值引用 可以move 后的左值取别名,不能直接给左值取别名

三、右值引用解决左值引用的缺陷

1. 左值引用的价值与缺陷

左值引用使用场景

  1. 输出型参数,减少大对象的拷贝
  2. 做返回值减少拷贝

价值: 减少拷贝

缺陷局部对象做返回值时,不能用左值引用返回,此时必须传值返回。如以下场景:

cpp 复制代码
string func() {
	std::string str;
	std::cin >> str;
    // ... 一系列操作
	return str;
}
  • 以上函数传值返回,在没有右值引用时 ,需要拷贝资源,存在拷贝资源的性能开销

  • 局部string返回string&时存在的两层问题:

  • 左值引用无法解决以上问题,该场景只能传值返回,性能开销较大

右值引用发明之前,以上场景只能传值返回


2. 传值返回时编译器RVO的优化

cpp 复制代码
string Func1() 
{
    string str;
    cin >> str;
    // ...
    return str;
}
void test4() 
{ 
    string ret1 = Func1(); 
}
  • 以上场景编译器不进行优化时,会发生两次深拷贝 ,每次深拷贝 都伴随着以下三个步骤:
    • 开空间

    • 深拷贝数据

    • 调用析构函数释放原来空间

以上操作叠加起来,时间代价极大


因此以上步骤被编译器优化为了一次拷贝构造同一个执行步骤中,进行连续的构造或拷贝构造,编译器会优化成一次直接构造.

  • 优化 :在 str 销毁前,直接用str拷贝构造ret1优化为一次拷贝构造

如下为有无优化的详解过程

如果没有优化(没有 RVO/NRVO / copy elision):

  1. 局部str → (深拷贝)→ 形成临时返回值对象
  2. 临时返回值对象 → (深拷贝)→ ret1

➡️一共可能有 两次构造开销(两次深拷贝构造 + 两次析构)。


如果有优化(现代编译器默认开启)

  • C++17 标准规定 :强制性返回值优化(RVO),即使返回值不加 std::movestr 也会被直接构造在调用者 ret1 的存储空间上。

  • 所以优化后直接使用局部str构造ret对象 ,只有 一次直接构造,没有临时对象。


✅ 结论:

  • 不做优化(老编译器/关闭优化) :会深拷贝生成一个临时对象 ,再调用一次深拷贝 构造 ret1
  • C++17 及之后标准 / 常见编译器优化开启时 :直接在 ret1 上构造,不会有临时对象,减少了深拷贝消耗

3. 无法优化的场景

以下场景无法被优化,会发生两次拷贝构造。

无法优化的原因 是:同一个执行步骤中,没有发生连续的构造或拷贝构造,编译器无法优化

cpp 复制代码
string Func1() {
    string str;
    cin >> str;
    // ...
    return str;
}

void test4() {
    string ret1;
    // ... 一系列操作
    ret1 = Func1();
}
  • 因为ret1 = Func1()不是连续的构造,也不是连续的拷贝构造 ,此处编译器无法优化。在没有右值引用的移动语义时,存在两次深拷贝的性能开销

4. 左值和右值的参数匹配

  • C++98中,函数参数为 const 左值引用 时,传实参时,左值和右值都可以匹配

  • C++11以后,分别实现参数为 左值引用、const 左值引用、右值引用 的函数,构成函数重载

    • 实参传入左值 :匹配左值引用,
    • 实参传入 const 左值 :匹配 const 左值引用
    • 实参传入右值 :匹配右值引用
  • 以下func1函数构成函数重载 :分别传入左值、const 左值、右值 时,会==调用参数最匹配的函数==

cpp 复制代码
// 以下三个函数构成函数重载 
void func1(int& r) {
	cout << "void func1(int& r)" << endl;
}
void func1(const int& r) {
	cout << "void func1(const int& r)" << endl;
}
void func1(int&& r2) {
	cout << "void func1(int&& r)" << endl;
}
void test5() {
	int a = 0;
	const int b = 20;
	func1(a);	// 这里两个func1都可以调用,但这里不会出现歧义,编译器会去调用最匹配的那个函数
	func1(b);
	func1(a + b);	// a + b 是右值,右值引用版本 最匹配,因此调用右值引用版本
}

四、右值引用的移动语义

右值有以下分类

  • 内置类型 的右值:认为是纯右值

  • 自定义类型 的右值:认为是将亡值

实现了简单移动赋值移动拷贝构造m_string类:

cpp 复制代码
// m_string.h

// 移动拷贝构造
string(string&& str) {
	cout << "string(string&& str) -- 移动拷贝构造" << endl;
	swap(str);
}

// 移动赋值
string& operator=(string&& str) {
	cout << "string& operator=(string&& str) -- 移动赋值" << endl;
	swap(str);
	return *this;
}
  • 移动构造 :把另一个右值将亡值对象的资源直接"偷"过来(交换指针),避免深拷贝。
  • 移动赋值 :把已有对象的资源释放,然后再接管右值将亡值对象的资源。

上述string类中移动赋值和移动构造的实现不够规范,移动赋值和移动构造函数不应抛异常,应给函数标上noexcept


1. 移动赋值

认识移动赋值

移动赋值运算符(Move Assignment Operator ):是实现移动语义的一个核心工具。

移动赋值运算符的核心用于将一个临时对象(右值)的资源"窃取"并赋值给一个已存在的对象,而非进行耗时的深拷贝赋值,从而优化性能。

cpp 复制代码
class MyClass
{
private:
    //1.动态分配的资源(如:数组、缓冲区等)
    //2.资源大小(如:数组长度)

    int* data;        
    size_t size;      

public:
        /*------------------------------移动赋值运算符的定义格式------------------------------*/
    //移动赋值运算符:通过右值引用接收临时对象,实现资源的高效转移 
    MyClass& operator=(MyClass&& other) noexcept  //noexcept关键字声明:承诺不会抛出异常(移动操作应避免抛异常,否则可能导致资源泄漏)
    {
        //1.自赋值检查:避免自己给自己赋值(如:a = std::move(a))
        if (this != &other) //注意:若不检查,delete[] data 会释放自己的资源,后续又从自己窃取资源,导致悬空指针
        {
            //2.释放当前对象的旧资源:防止内存泄漏
            delete[] data; //例如:当前对象持有动态分配的数组,需先释放

            //3.从other中"窃取"资源 + 置空原对象指针
            data = other.data;         // 接管指针(如:动态数组、文件句柄等)
            other.data = nullptr;      // 置空原对象指针:防止原对象析构时重复释放资源

            //4.复制资源元数据 + 重置原对象状态
            size = other.size;
            other.size = 0;     //确保原对象析构时不会影响已转移的资源
            //注意:原对象仍需保持可析构状态(如:调用默认析构函数不会崩溃)
        }

        //5.返回当前对象引用
        return *this; //支持链式赋值(如 a = b = c)
    }
};

移动赋值运算符的核心逻辑

  • 参数类型:移动赋值运算符同样接收一个右值引用参数

  • 自我赋值检查if (this != &other) 避免自己赋值给自己时出错

  • 释放旧资源 :先删除当前对象的资源(delete[] data),避免内存泄漏

  • 资源转移:同移动构造函数,接管原对象的资源并重置原对象


有RVO时的移动赋值
cpp 复制代码
m_string::string test6() {
	m_string::string str("xxxxxxxxxxxxxxxxxxx");
	// ...
	return str;
}
void test7() {
	m_string::string ret2;
	// ...
	ret2 = test6();
}

调用分析

调用 test6()ret2 接收test6的返回值

  1. 如果编译器开启返回值优化 (RVO / NRVO)

    • 会用 str 直接在 ret2 的存储区构造一个右值 m_string::string 对象,不会发生拷贝/移动,不调用移动构造仅发生一次直接构造
  2. 如果编译器禁用返回值优化 (关闭 -fno-elide-constructors),且实现了 string 的移动构造

    • 由于 str 是局部变量,返回时会被视为右值将亡值 ,会先用 str 调用移动构造生成临时对象,临时对象再作为右值返回给ret2之后调用移动赋值

    • ret2 = test6();test6() 返回的临时对象右值 ,因此调用的是 移动赋值运算符

    cpp 复制代码
      string& operator=(string&& str);
    • 移动赋值 中,swap(str)ret2 的资源和右值对象的资源交换,ret2 就拥有了右值对象的内容。

    • 最后 :临时右值对象(test6() 的返回值)生命周期结束,被析构,但它已经被 swap 交换成空壳,所以析构代价极小。


移动赋值/移动构造窃取资源时的调用流程:


无RVO时的移动赋值
cpp 复制代码
m_string::string test6() {
	m_string::string str("xxxxxxxxxxxxxxxxxxx");
	// ...
	return std::move(str);
}
void test7() {
	m_string::string ret2;
	// ...
	ret2 = test6();
}

调用过程分析

禁用编译器的 RVO 优化后,以上过程分为两步完成:

  1. 由于 str右值将亡值调用移动拷贝构造,构造出一个临时对象
  2. 临时对象为右值将亡值调用移动赋值,赋值给ret2

2. 移动构造

认识移动构造

移动构造函数(Move Constructor):是一种特殊的构造函数,用于实现"移动语义"。

移动构造函数的核心将一个临时对象(右值)的资源"窃取"过来为己所用,而不是进行耗时的深拷贝,从而提高程序性能。

  • 尤其适用于管理动态内存、文件句柄等资源的对象
cpp 复制代码
class MyClass
{
private:
   //1.动态分配的资源(如:数组、缓冲区等)
   //2.资源大小(如:数组长度)

   int* data;        
   size_t size;      

public:
   /*------------------------------移动构造函数的定义格式------------------------------*/
   //移动构造函数:通过右值引用接收临时对象,实现资源的高效转移
   MyClass(MyClass&& other) noexcept //noexcept关键字声明:承诺不会抛出异常(移动操作应避免抛异常,否则可能导致资源泄漏)
   {
       // 核心逻辑:将临时对象(右值)的资源所有权转移到当前对象,而非复制资源
       /*--------------第一步:接管资源--------------*/

       data = other.data; //直接获取原对象的指针(无需分配新内存,避免深拷贝)

       /*--------------第二步:置空原对象指针--------------*/

       other.data = nullptr; //防止原对象析构时释放已转移的资源(必须操作)
       //注意:这一步使原对象进入"有效但可析构"的状态(通常称为"被移动状态")

       /*--------------第三步:复制资源元数据--------------*/

       size = other.size; //例如:大小信息


       /*--------------第四步:重置原对象状态--------------*/

       other.size = 0; //确保原对象析构时不会影响已转移的资源
       //注意:原对象仍需保持可析构状态(如:调用默认析构函数不会崩溃)
   }
};

移动构造函数的核心逻辑

  • 参数类型必须是右值引用 例如 MyString&&确保只接收右值(临时对象)
  • 资源转移 :直接拷贝原对象的资源指针(data=other.data ),而非重新分配内存
  • 重置原对象 :将原对象的指针置空(other.data = nullptr ),避免析构时双重释放资源
  • noexcept关键字:声明函数不会抛出异常,使容器(如:vector)在扩容时优先选择移动而非拷贝(提升性能)

编译器不做优化时的移动构造
cpp 复制代码
m_string::string test6() {
	m_string::string str("xxxxxxxxxxxxxxxxxxx");
	// ...
	return str;
}
void test7() {
	m_string::string ret1 = test6();	// 连续的拷贝和构造  优化为直接构造
}

编译器优化后的移动构造
cpp 复制代码
m_string::string test6() {
	m_string::string str("xxxxxxxxxxxxxxxxxxx");
	// ...
	return std::move(str);
}
void test7() {
	m_string::string ret1 = test6();	// 连续的拷贝和构造  优化为直接构造
}

五、右值引用和移动语义的应用场景

编译器做的优化

  • 把传值返回的需要深拷贝自定义类型,强行识别成右值
  • 这样就可以调用移动构造和移动赋值,窃取资源,避免拷贝

1. 自定义类型中深拷贝的类,必须传值返回的场景

std::move的意义就在于,可以让一个对象的资源可以被抢走

右值引用配合移动语义,可以极大地提高性能,减少深拷贝


代码片段

cpp 复制代码
m_string::string test6() {
	m_string::string str("xxxxxxxxxxxxxxxxxxx");
	// ...
	return std::move(str);
}

m_string::string ret1 = test6();	// 移动拷贝

m_string::string ret3("1111111111111111111111");
m_string::string copy1 = ret3;	// 深拷贝

move(ret3);	
m_string::string copy2 = ret3;	// 深拷贝

m_string::string copy3 = move(ret3);	// 移动拷贝

运行结果分析




移动语义的核心:资源抢占

当我们写:

cpp 复制代码
m_string::string copy3 = std::move(ret3); // 移动拷贝

这时发生的事情是:

  1. std::move(ret3)ret3 转为右值引用;
  2. 编译器匹配到移动构造函数 (形如 string(string&& s));
  3. 在移动构造函数中,copy3 不会重新分配内存,而是直接"接管 " ret3 的资源指针;
  4. 同时将 ret3 的内部指针置为 nullptr,防止重复释放。

这种资源"抢占"让构造过程几乎为 O(1),极大地减少了内存开销。


2. STL 容器的插入接口

STL 容器的插入接口,如果插入对象是右值,可以利用移动构造或移动赋值转移资源给数据结构中的对象,减少深拷贝

如以下代码场景

cpp 复制代码
std::list<m_string::string> lt;
m_string::string s1("111111111111111111111");
lt.push_back(s1);

cout << endl;

m_string::string s2("111111111111111111111");
lt.push_back(move(s2));

cout << endl;

lt.push_back("22222222222222222222222222");

C++98中的调用过程


C++11中的配合move产生右值


右值的日常写法

  • 日常写法一般不会先构造出一个左值 ,再对左值加std::move,而是会直接构造一个匿名对象,匿名对象为右值

深拷贝和移动拷贝的对比



六、引用折叠与万能引用

1. 引用折叠

引用折叠 :是 C++11 引入的一个重要特性,它和模板、右值引用等概念紧密相关,在一些复杂的类型推导场景中发挥着关键作用。

在 C++ 中

  • 直接定义 "引用的引用" ,比如:int& && r = i;i 是一个 int 类型变量)是不被允许的,会直接导致编译报错
  • 但在模板实例化 或使用 typedef 进行类型操作时,可能会出现多个引用叠加的情况,这时候就需要引用折叠规则来确定最终的引用类型

简单来说引用折叠就是在特定情况下,将多个连续的引用类型 "折叠" 成一个引用类型


当通过 模板/typedef 构造出 "引用的引用" 时,C++11引用折叠规则统一处理:

  • T& &T& (左值引用的左值引用 ---> 折叠为 ---> 左值引用 )
    T& &&T& (右值引用的左值引用 ---> 折叠为 ---> 左值引用 )
    T&& &T& (左值引用的右值引用 ---> 折叠为 ---> 左值引用 )
    T&& &&T&& (右值引用的右值引用 ---> 折叠为 ---> 右值引用)

引用折叠规则总结:

  • 右值引用的右值引用折叠成右值引用
  • 其他组合均折叠成左值引用

2. 模板的万能引用

模板 中的 && 不代表右值引用,而是万能引用 ,其既能接收左值又能接收右值

cpp 复制代码
template <typename T>
void func(T&& t) 
{ 
    /* ... */
}

这里的 T&& t 看起来是 "右值引用参数",但结合引用折叠规则 后,它会根据传入实参的类型 "自适应"

由引用折叠规则可知

  • 传入左值时T 会被推导为 左值引用类型 ,即T = int&。于是 T&& 展开为 int& &&,按引用折叠规则变为 int&。因此 t 的类型为 左值引用
  • 传入右值时T 会被推导为 非引用类型 ,即T = int,所以 T&&int&&,即 右值引用

这种能同时适配左值、右值的模板参数,也被称为万能引用 (或 "转发引用")------ 靠模板推导T 变成 t&(左值)或 t(右值)。

总结当模板函数中出现T&&

  • 实参是左值t 的类型就是左值引用
  • 实参是右值t 的类型就是右值引用

万能引用的例子

cpp 复制代码
//万能引用
template<class T>
void Function(T&& t)
{
	int a = 0;
	T x = a;
	//x++;
	cout << &a << endl;
	cout << &x << endl << endl;
}
 
int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(10); // 右值
 
	int a;
	// a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
	Function(a); // 左值
 
	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(std::move(a)); // 右值
 
	const int b = 8;
	// b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
	Function(b); // const 左值  	所以Function内部 进行 x++ 操作时, 会编译报错
 
	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
	Function(std::move(b)); // const 右值		所以Function内部 进行 x++ 操作时, 会编译报错
 
	return 0;
}

综上,万能引用在接受左值和右值时:

  • 被传左值就实例化左值引用版本
  • 被传右值就实例化右值引用版本

这就是模板的万能引用


七、完美转发及其应用

1. 左值引用和右值引用本身均是左值

右值引用类型的变量在用于表达式时,属性是左值 。原因是**在移动构造的场景下,右值引用必须要支持修改,否则无法完成资源转移**

这个设计这里会感觉很怪,等我们讲右值引用及完美转发的使用场景时,就能体会这样设计的价值了

右值的两个本质特征

  • 右值不能取地址
  • 右值不能修改

右值引用本身也是一个变量,这个变量本身的属性是左值,如下代码验证了这个结论:

cpp 复制代码
int a = 19;
int& r = a;	
int&& rr = std::move(a);

cout << &r << endl;
cout << &rr << endl;

由上可知

  • 左值引用类型的变量r可以取地址,可以修改 ,因此左值引用类型的变量的属性是左值

  • 右值引用类型的变量rr可以取地址,可以修改 ,因此右值引用类型的变量的属性是左值


总结

左值引用和右值引用本身均是左值

  • 左值引用,引用的对象为左值,左值引用类型的变量,本身是左值

  • 右值引用,引用的对象为右值,但右值引用类型的变量,本身是左值


2. 完美转发及其应用

什么是完美转发?

完美转发 :是 C++11 及以后引入的一项重要特性,它允许函数模板将其参数原封不动地转发给另一个函数 ,保持参数的 原始值类别 (左值或右值)和 常量性

  • 完美转发 主要借助 引用折叠std::forward 实现,用于在函数模板中精准保留参数的 "左值/右值属性",并将其原封不动地转发给下一层函数

为什么需要完美转发?

右值引用的"左值属性"问题

  • 结合变量表达式的规则 (所有变量表达式都是左值)
  • 即使右值被右值引用(T&&)绑定,右值引用变量本身仍属于左值(可被取地址、赋值等)

这会导致一个问题:

  • PerfectForward 函数内部,若把 t 传递给下一层函数 Funct 会被当作左值处理只能匹配 Func 的左值引用版本
  • 但我们希望保留t原始的"左值/右值属性" (比如:让右值继续匹配Fun的右值引用版本),这时就需要 完美转发 来解决

示例问题(无完美转发时)

cpp 复制代码
#include <iostream>
using namespace std;

void Func(int& x) { cout << "左值引用版本\n"; }
void Func(int&& x) { cout << "右值引用版本\n"; }

template<typename T>
void PerfectForward(T&& t)
{
    Func(t);        //t 是右值引用变量,但本身是左值 → 调用 Fun(int&)
}

int main()
{
    PerfectForward(10); //传递右值下去,期望调用 Fun(int&&),实际调用 Fun(int&)
    return 0;
}

如何实现完美转发?

完美转发 通过 std::forward 函数模板实现,核心依赖 引用折叠模板参数推导


std::forward 的简化原理
std::forward利 用 引用折叠模板参数 推导,"还原"参数原始的左值/右值属性

cpp 复制代码
// std::forward函数模板的关键逻辑可简化实现为:

template <typename T>
T&& forward(typename remove_reference<T>::type& arg) 
{
	return static_cast<T&&>(arg);  // 强制类型转换,触发引用折叠
}

完美转发的流程

PerfectForward(10) 为例,完整流程:

  • 模板参数推导 :传入右值 10 →PerfectForwardT 推导为 int(非引用类型),形参 tint&&右值引用
  • 调用 std::forward :在 PerfectForward中调用 Func(std::forward<T>(t)) → Tint类型, std::forward<int>(t) 会:
    • 通过 static_cast<int&&>(t)t 强转为 int&&(右值引用
    • 触发 Func(int&&)重载,实现保留右值属性

完美转发保持对象的原生属性

  • Fun(t)调用修改为Fun(forward<T> (t)):即可利用完美转发 保持所引用对象的左值属性或右值属性
cpp 复制代码
#include <iostream>
using namespace std;

void Func(int& x) { cout << "左值引用版本\n"; }
void Func(int&& x) { cout << "右值引用版本\n"; }

template<typename T>
void PerfectForward(T&& t)
{
    //Func(t);        //t 是右值引用变量,但本身是左值 → 调用 Fun(int&)
    Func(std::forward<T> (t));  //t 是右值引用变量,本身是左值,利用完美转发保持右值属性,调用 Func(int&& x)
}

int main()
{
    PerfectForward(10); //传递右值下去,利用完美转发保持所引用的右值对象的右值属性,调用 Func(int&& x)
    return 0;
}

总结完美转发的使用:

cpp 复制代码
template<typename T>
void PerfectForward(T&& t)
{
	//Fun(t);
    
	// 完美转发保持对象的原生属性
	// 完美转发: 引用折叠后 t 是左值引用,完美转发后,t保持左值属性
	// 完美转发: 引用折叠后 t 是右值引用,完美转发后,t保持右值属性
	Fun(forward<T> (t));
}

3. 完美转发的应用场景

  • 标准库中的list可以调用移动构造
  • 我们自己实现的list无法调用我们的string实现的移动构造

  • 构造lt的时候,构造了哨兵位的头结点 ,因此string一共是四次构造函数
  • 之前list实现的push_backinsert,参数类型为 const引用左值和右值都会调用const左值引用版本的push_back和insert
cpp 复制代码
void push_back(const T& val) 
{
	insert(end(), val);
}
iterator insert(iterator pos, const T& val) 
{
	Node* newNode = new Node(val);
	newNode->_next = pos._node;
	newNode->_prev = pos._node->_prev;
	pos._node->_prev->_next = newNode;
	pos._node->_prev = newNode;
	++_size;
	return iterator(newNode);	// 返回新插入元素的位置
}

补充实现右值引用版本的push_backinsert,增加和修改以下函数

cpp 复制代码
void push_back(T&& val) {
	insert(end(), std::forward<T> (val));
}
iterator insert(iterator pos, T&& val) 
{
	Node* newNode = new Node(std::forward<T> (val));
	newNode->_next = pos._node;
	newNode->_prev = pos._node->_prev;
	pos._node->_prev->_next = newNode;
	pos._node->_prev = newNode;
	++_size;
	return iterator(newNode);	// 返回新插入元素的位置
}

// 结点创建时的构造函数
list_node(const T& val)
	:_next(nullptr)
	, _prev(nullptr)
	, _val(val)
{ }

list_node(T&& val)
	:_next(nullptr)
	, _prev(nullptr)
	, _val(std::forward<T> (val))
{}

// 由于node的构造函数去掉了默认参数,因此此处传入一个匿名对象
void empty_init() {
	_head = new Node(T());	// 匿名对象为右值,构造 Node 时会匹配右值版本的构造函数
	_head->_next = _head;
	_head->_prev = _head;
	_size = 0;
}

需要注意的是

  • 我们期望调用右值引用版本push_back,而push_back是借助 insert 实现的,push_back函数中将待插入的值传递到了 insert 函数中去。而右值引用变量本身是左值 ,如果想调用右值引用版本的 insert 函数,需要在 push_back insert 传参时使用完美转发,保持对象的右值属性 ,才能匹配到右值引用版本insert 函数
  • insert 中,右值引用 val 被向下传递给了 Node 的构造函数,我们期望调用右值引用版本Node 构造函数,因此同样需要使用完美转发向下传递参数
  • Node 的构造函数对 val 使用初始化列表初始化成员变量_val,会调用 _val 类型的默认构造进行初始化,为了确保能够调用右值引用版本的移动构造 ,传参时需要使用完美转发保持 val 对象的右值属性

修改后结果如下


八、std::move VS std::forward

1. 右值引用 对比 万能引用

对比项 右值引用(Rvalue Reference) 万能引用(Forwarding Reference)
语法形式 Type&&类型已确定 T&&(其中 T 是模板参数并可推导
是否依赖类型推导 ❌ 否 ✅ 是
是否发生引用折叠 ❌ 否 ✅ 会发生(T& && → T&T&& && → T&&
能绑定的实参类型 仅能绑定右值 既能绑定左值也能绑定右值
典型使用场景 移动构造函数、移动赋值、接收右值对象 泛型模板函数、完美转发
函数形参的值类别 始终是右值引用,但在函数体内是左值 随实参推导:左值 → 左值引用;右值 → 右值引用
在函数内部的传递方式 使用 std::move(param) 强制右值化 使用 std::forward<T>(param) 保持实参的值类别
语义目标 启用移动语义(资源转移) 实现完美转发(不改变值类别)
传递结果 始终右值 与实参值类别一致
风险 被移动对象不可再安全使用 无资源转移风险,仅保持原值类别
常见示例 移动构造函数、容器的移动插入 emplace_backpush_back 等模板接口

2. std::move 与 std::forward 的使用场景总结:

  • std::forward

当形参类型是万能引用T&&,其中 T 是模板参数并**可推导**),在函数内部将该形参传递给其他函数时 ,应使用
std::forward<T>(param) 来**保持实参的值类别(左/右值)**。

换句话说,完美转发的意义在于"不改变传入对象的左/右值属性"


  • std::move

当形参类型是普通右值引用Type&&,其中 Type 是**已确定类型、不可推导**)时,在函数内部将该形参传递给其他函数时 ,应使用
std::move(param) 将其**显式转换为右值,从而触发移动语义**。

换句话说,std::move 的意义在于:"告诉编译器:此对象的资源可以被转移,不再需要保持原值。"


结语

右值引用、移动语义与完美转发的出现,标志着 C++ 从"拷贝一切"的传统模型迈向了"高效资源转移"的新时代。

它们不仅优化了对象的构造与赋值过程,更让泛型代码在类型推导与参数传递中保持了语义一致性与性能最优

理解这些概念后,我们在写代码时:

  • 知道何时该深拷贝,何时该移动
  • 明白模板函数如何无损转发参数
  • 能在 STL 与自定义容器中正确利用移动构造与完美转发。

这不仅是一场语法的革新,更是 C++ 性能哲学的进化。

让我们带着对效率与优雅的追求,继续探索现代 C++ 的更多可能。


以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步

分享到此结束啦
一键三连,好运连连!

你的每一次互动,都是对作者最大的鼓励!


征程尚未结束,让我们在广阔的世界里继续前行! 🚀

相关推荐
jz_ddk2 小时前
[实战] 卡尔曼滤波原理与实现(GITHUB 优秀库解读)
算法·github·信号处理·kalman filter·卡尔曼滤波
啊吧怪不啊吧2 小时前
一维前缀和与二维前缀和算法介绍及使用
数据结构·算法
草莓熊Lotso2 小时前
《算法闯关指南:优选算法--位运算》--36.两个整数之和,37.只出现一次的数字 ||
开发语言·c++·算法
Le1Yu2 小时前
微信小程序端服务器接口:全部服务以及实战
微信小程序·小程序
乌萨奇也要立志学C++2 小时前
【Linux】进程间通信(二)命名管道(FIFO)实战指南:从指令操作到面向对象封装的进程间通信实现
linux·服务器
此生只爱蛋3 小时前
【Linux】自定义协议+序列和反序列化
linux·服务器·网络
铭哥的编程日记3 小时前
深入浅出蓝桥杯:算法基础概念与实战应用(一)基础算法(上)
算法·职场和发展·蓝桥杯
小年糕是糕手3 小时前
【数据结构】常见的排序算法 -- 选择排序
linux·数据结构·c++·算法·leetcode·蓝桥杯·排序算法
huangyuchi.3 小时前
【Linux网络】Socket编程实战,基于UDP协议的Dict Server
linux·网络·c++·udp·c·socket