【C++移动语义与完美转发】左值右值,引用,引用折叠,移动语义,万能引用与完美转发

前言

  • nav2系列教材,yolov11部署,系统迁移教程我会放到年后一起更新,最近年末手头事情多,还请大家多多谅解。
  • 本期是一个鸽了半年的教程,很早以前我就一直想写一篇文章有关C++的移动语义,一直拖到现在(),那么今天我们就来看看到底怎么一个事情。

1 左值右值和值类别

1-1 左值右值(粗分类)
  • 大家也许曾经粗略听过有关左值右值的大致概念:(没有也没事,下面我会详细讲)
    • lvalue(左值)代表具有持久性的对象或变量,它可以表示一个有明确内存位置的对象,可以取地址,并且可以出现在赋值语句的左边。一个左值可以取地址,具备可修改性。
    • rvalue(右值)是一个表示临时对象的表达式,通常是没有持久地址的“值”,它通常出现在赋值语句的右边。rvalue 是短暂的,一旦计算完成就会消失。而右值通常不允许被直接修改,它代表的是一个只读的对象。
  • 但实际上,在C++11中,引入了值类别的概念,进一步划分了值的类别,总览如下图请添加图片描述
1-2 值类别
  • 正如cppreference.com提到的,在C++中,每个表达式都有其对应的类型和值类别请添加图片描述

  • 而这些值类别可以划分为三个基础(主要)的类别,分别是 lvalue,xvalue,prvalue

  • 而在这之上又可以分为gvaluervalue两个大类,如上图,接下来我们就来详细看看每一个类别分别代表啥。

1-3 左值lvalue
  • 正如上面粗分类的一样,lvalue 是表示一个可以持久存在且可以通过引用修改的对象的表达式。简单来说,lvalue“有名字的对象”“可取地址的对象”。它可以在程序的不同地方被引用或修改。
  • 常见的lvalue
  1. 变量: 任何具名的变量(如 int x = 10; 中的 x)都是 lvalue。
  2. 数组元素: 数组元素也是 lvalue,因为它们有明确的内存位置。
int arr[5] = {1, 2, 3, 4, 5}; arr[2] = 10;  // arr[2] 是 lvalue
  1. 函数返回值(如果返回类型为 lvalue 引用): 如果函数返回类型是 lvalue 引用,那么调用该函数的结果就是一个 lvalue。(一般的函数返回值是一个右值实际上)。注意下面这个代码函数返回值是左值,因此必须返回一个右值(比如说静态成员),否则代码会出错!!!
int& getValue() {   static int x = 10;    return x; 
}  
getValue() = 20;  // getValue() 返回 lvalue,可以赋值
  1. 成员访问: 类成员可以通过 .-> 操作符访问,它们是 lvalue(如果成员本身是可以修改的)。
struct foo 
{     int m; 
};  
foo a;
a.m = 42;  // a.m 是 lvalue
  1. 非静态数据成员: 访问非静态成员也会返回 lvalue。
  2. 表达式中的左操作数: 类似于 a = ba += b++a--a,它们的左操作数是 lvalue。
int a = 5; 
a += 3;  // a 是 lvalue
  1. 成员指针访问: 如果你通过成员指针访问数据成员(a.*mpp->*mp),它们也是 lvalue。
  2. 字符串字面量: 字符串字面量本身被视为 lvalue。
const char* str = "Hello, world!";  // "Hello, world!" 是 lvalue
  1. 模板参数(lvalue 引用类型): 如果模板参数是 lvalue 引用类型,它也是一个 lvalue。
struct foo {};template <foo a>
void baz()
{const foo* obj = &a;  // `a` is an lvalue, template parameter object
}

  • lvalue的特性
  1. 可以取地址: lvalue 表达式代表一个内存位置,因此可以使用地址运算符 & 取其地址。
int x=5;
int* p=&x;// x 是 lvalue,可以取地址
  1. 可以作为赋值的左操作数: lvalue 允许在赋值操作的左边使用。
int x=5;
x=10;    // 'x' 是 lvalue,允许在赋值操作符左边
  1. 可修改: lvalue 代表的对象通常是可修改的,可以通过左值引用(可以理解为对于左值的引用)修改该对象。(引用本文后面会讲,这里先提一嘴)
int x = 5;
int &y = x;      // x 被修改为 10`
  1. 表示持久存在的对象: lvalue 通常表示具有明确内存位置的对象,如变量、数组元素或解引用的指针。它们的生命周期通常贯穿整个作用域。
  2. 能够作为初始化左值引用的对象: 一个lvalue 可以用来初始化 lvalue 引用,这将为该对象创建一个新的名称。
  3. 成员访问: 通过成员访问操作符 (.->),可以访问对象的成员,这些操作返回 lvalue。例如,通过 a.m 访问对象 a 的成员 ma.m 就是一个 lvalue,表示对象成员的地址。
struct foo { int m; 
};  
foo a; 
a.m = 10;  // a.m 是 lvalue

1-4 纯右值prvalue
  • 同理,纯右值(prvalue,pure rvalue)是 C++ 中一种表达式类型,它主要代表临时对象或者值计算的结果。简单来说,纯右值是没有持久化位置的表达式,它通常是一个临时值,用于初始化或赋值给变量。与左值(lvalue)不同,纯右值无法被赋值(即不能出现在赋值操作符的左边),它们仅仅代表值而没有持久化存储位置。
  • 纯右值的常见类型包括:
  1. 字面量:例如整数常量 42(宇宙的答案!!!!!)、布尔值 truenullptr。这些是直接表示常量值的表达式。
int x = 42;  // 42 是纯右值
  1. 函数调用和运算符表达式:当函数的返回类型是非引用类型时,或者运算符表达式的结果是非引用类型时,都是纯右值。例如,str.substr(1, 2) 或者 str1 + str2 都是纯右值。
    • 所以你不能给返回值为纯右值的函数赋值,你不能str.substr(1, 2)="Hello";
std::string str = "Hello";
auto substr = str.substr(1, 2);  // substr(1, 2) 返回的是一个临时值,属于纯右值
  1. 自增与自减运算a++a-- 操作会产生一个临时值,该值是纯右值。请添加图片描述
int a = 10;
int b = a++;  // a++ 返回一个临时值,b 得到这个值,a 的值自增
  1. 算术运算表达式:像 a + ba % ba << b 等算术运算表达式都会产生纯右值。
    请添加图片描述
int a = 3, b = 4; 
int c = a + b;  // a + b 计算的结果是纯右值
  1. 逻辑运算表达式:例如 a && ba || b!a 等,都会产生纯右值。
bool a = true, b = false;
bool result = a && b;  // a && b 结果是纯右值
  1. 比较运算表达式:如 a < ba == b 等,比较操作会产生纯右值。
int a = 5, b = 10; 
bool result = a < b;  // a < b 结果是纯右值
  1. 地址运算符 &a``:取地址运算符 & 也可以生成纯右值,表示的是一个临时指针。
 int a = 10; int* ptr = &a;  // &a 是纯右值,表示一个临时指针`
  1. 成员访问表达式:例如 a.mp->m,如果 ap 是对象或指针,访问其成员会生成一个纯右值。
struct MyStruct {     int value; 
}; MyStruct obj = {42}; 
int val = obj.value;  // obj.value 作为纯右值
  1. 条件(三元)运算符:条件表达式 a ? b : c,当 bc 是纯右值时,整个表达式也是纯右值。
int a = 5, b = 10, c = 15; 
int result = (a < 10) ? b : c;  // b 或 c 是纯右值
  1. 类型转换表达式:例如 static_cast<double>(x)std::string{}(int)42,这些类型转换表达式也可以生成纯右值。
double x = static_cast<double>(5);  // static_cast<double>(5) 是纯右值
  1. Lambda 表达式:例如 [](int x){ return x * x; },它会生成一个临时的 Lambda 对象,也是纯右值。
auto lambda = [](int x) { return x * x; };  // lambda 表达式是纯右值
  1. 模板参数:非类型模板参数(如 template <int v> void foo() 中的 v)也是纯右值。
template <int v> 
void foo() 
{     // v 是纯右值,无法直接绑定到 lvalue 引用 
}

  • 纯右值的特性:
  1. 不可变性:纯右值不能被修改(因为它们没有持久的内存位置)。例如,不能将纯右值赋值给一个变量,除非通过右值引用进行绑定。(这个我们本文后面会讲)
  2. 不具备多态性:纯右值没有多态性,其表示的对象类型总是该表达式的类型,而不是派生类的类型。
  3. 非类类型的纯右值:对于非类类型的纯右值,它不能有 constvolatile 限定符,除非经过某种转换(如绑定到 const 引用)。
  4. 不完整类型:纯右值不能有不完整的类型(除了 void 或用于 decltype 中的类型)。
  5. 临时性:纯右值代表的是一个临时的、一次性的值,它不会在程序中长期存在。

1-4 将亡值xvalue–即将失效的右值
  • 这可是一个新概念,我们来看看。
  • xvalue(expiring value)是一个表达式类型,表示的是临时对象或即将失效的资源,通常与资源管理或移动语义密切相关。==xvalue 介于左值(lvalue)和右值(rvalue)之间,既具有右值的特性,也具有某些左值的特性。==xvalue 的出现主要是为了支持资源的“移动”,它与 C++ 的移动语义密切相关。

  • 关于xvalue的举例,常见的例子有 std::move() 表达式、数组元素的访问等。这里涉及两个知识点
    • std::move
    • 右值引用&&
  • 这两我们本文后面会将,这里我们先跳过。

  • 总之你先记得!!!!!!!!!!
  1. xvalue 是右值:==每个 xvalue 都是一个右值(rvalue),但并不是所有的右值都是 xvalue。==xvalue 是右值的一种特殊类型,它表示一个可以被“消耗”或“移动”的对象。
  2. 即将失效的资源:xvalue 通常用于表示那些可以被转移、销毁或不再需要的资源。它们通常表示临时对象或需要被释放的资源。
  3. 与移动语义相关:xvalue 是 C++11 移动语义的核心,它常常用于实现对象的“移动”而不是复制,避免不必要的开销。xvalue 经常与 std::move 一起使用。(这个我们本文后面会讲)

1-5 广义左值glvalue和右值rvalue
  • 讲完了三个基础类型,我们往上走一级,我们来看看广义左值glvalue和右值rvalue请添加图片描述

  • 广义左值(Generalized Lvalue,简称 glvalue)是 C++ 中的一个类型类别,它可以是左值(lvalue)即将失效的右值(xvalue)。广义左值本质上代表了可以被访问或修改的内存位置。

    • 可被取地址:广义左值可以使用内建的 & 运算符来取地址(但并非所有 glvalue 都能通过 & 取地址,xvalue 就不能取地址)。
    • 可能是多态的:广义左值的动态类型可以与表达式的静态类型不同,这意味着它可能涉及到虚函数调用等多态行为。
    • 支持不完整类型:广义左值可以有不完整类型(比如前向声明的类类型),只要 C++ 允许该类型的表达式出现。
  • 右值(rvalue)是表示临时对象或值的表达式,通常是无法再继续引用或修改的对象。在 C++ 中,右值可以进一步划分为 纯右值(prvalue)即将失效的右值(xvalue)。C++11 引入了右值引用和移动语义,允许右值成为对象资源管理的关键工具。

    • 地址不能被取:右值(无论是纯右值还是 xvalue)的地址不能通过内建的 & 运算符取,除非它绑定到一个引用(如右值引用)。
    • 不可作为赋值运算符的左操作数:右值不能作为赋值运算符的左操作数。赋值运算符左操作数必须是左值。

1-6 左值右值和值类别小节
  • 上面废话这么多其实就是在广义分类左值右值中,C++11又引入了一个新的xvalue将亡值,也就是即将失效的右值(xvalue)。这个xvalue通常和本文重点要提到的std::move和右值引用&&相关,因此我们接下来分别看看这两分别是啥。

2 引用,左右值引用,引用折叠

  • 没事不要慌,好玩的马上要来了!!
2-1 引用&
  • 在 C++ 中,引用(Reference) 是一种非常重要的语言特性,它提供了一种间接访问变量的方式。引用本质上是某个对象的别名,它允许通过不同的名称访问同一个内存地址的变量。引用广泛应用于函数参数传递、对象赋值、返回值优化等场景,能够提高代码效率、简化代码结构。
  • C++ 中的引用有多种类型,主要包括 左值引用(Lvalue Reference)右值引用(Rvalue Reference),并且每种引用都有其独特的特性和用途。
2-2 左值引用(Lvalue Reference)&
  • 左值引用是 C++ 中最常见的引用类型,它指向一个左值(即具有持久地址的对象)。左值引用通过引用符 & 来声明。
  • 语法:
T& ref = obj;  // T 为对象类型,ref 是左值引用,obj 是一个左值对象
  • 特性:
    • 绑定左值:左值引用只能绑定到左值(拥有持久地址的对象),不能直接绑定到右值。(废话)
    • 可修改引用的对象通过左值引用修改引用对象的值会直接影响原对象。
  • 我们来看一个典型的例子:
#include <iostream>void modifyValue(int& x) {x = 10;  // 通过引用修改原对象的值
}int main() {int a = 5;int& ref = a;  // ref 是 a 的左值引用ref = 7;  // 通过引用修改 a 的值std::cout << "a = " << a << std::endl;  // 输出: a = 7modifyValue(a);  // 通过引用修改 a 的值std::cout << "a = " << a << std::endl;  // 输出: a = 10return 0;
}
  • 我们直接看结果,refa 的左值引用,任何通过 ref 的修改都会直接影响 a,并且 modifyValue 函数通过引用传递参数,也修改了 a 的值。

  • 请添加图片描述

  • 常见用途:

    • 函数参数传递:传递大对象时,使用左值引用可以避免不必要的复制。
    • 对象修改:可以通过引用修改原始对象的值。

2-3 常量左值引用(Const Lvalue Reference)
  • 我们再来看看另一个常用的东西
  • 常量左值引用是左值引用的一个变种,它只能绑定到左值,并且绑定后无法修改引用的对象。常量引用通常用于传递对象时,既避免了复制,又能保护对象不被修改。
const T& ref = obj;  // const T& 表示常量左值引用
  • 特性:
    • 只能绑定左值:常量左值引用只能绑定到左值。(废话)
    • 不可修改对象通过常量引用无法修改绑定的对象。
  • 举个栗子:
#include <iostream>void printValue(const int& x) {std::cout << "Value: " << x << std::endl;// x = 10;  // 错误,不能修改常量引用的对象
}int main() {int a = 5;const int& ref = a;  // ref 是 a 的常量左值引用printValue(a);  // 通过引用传递对象,但不能修改return 0;
}
  • 常见用途:
    • 传递大对象:避免复制同时保证对象不被修改,常常用于传递临时对象或者大型对象。
    • 函数参数传递:提高函数效率,特别是当对象类型较大或是不可复制的类型时(如 std::stringstd::vector)。

2-4 左值引用和指针
  • 聪明的你一定发现了,这左值引用不就很像指针吗????
  • 相同点:
    1. 都可以间接访问对象
      • 左值引用和指针都能间接地引用(或访问)一个对象,允许通过它们访问原始对象。
    2. 都可以修改对象的值(如果没有加const
      • 如果引用或指针没有被声明为const,它们都可以修改所引用的对象的值。
  • 不同点:
特性左值引用(lvalue reference)指针(pointer)
语法T&(如:int& a = b;T*(如:int* p = &b;
是否可以为nullptr不可以为nullptr,必须绑定到一个有效的对象。可以为nullptr,即指向一个不存在的对象。
是否需要解引用无需解引用,直接通过引用访问对象。需要解引用(*p)才能访问指针所指向的对象。
是否支持改变绑定目标不可以改变绑定的目标,一旦绑定就不可再指向其他对象。可以修改指针的值,使其指向其他对象。
引用是否为对象的别名是的,引用是某个对象的别名(没有中介)。指针是一个变量,保存地址,指向对象。
内存管理不需要显式的内存管理。需要手动管理(例如使用new/deletemalloc/free)。
空指针检查无法为空,所以不需要检查是否为空。需要检查是否为空(例如if (ptr != nullptr))。
可以为数组或类的成员引用可以用作数组元素的引用或类成员的引用。可以通过指针指向数组或类的成员。
  • 使用场景:
  1. 左值引用(lvalue reference)
    • 传递对象引用:左值引用常用于函数参数传递,尤其是当你希望避免不必要的复制时(特别是对于大型对象),同时又不想改变参数的值。
    • 避免不必要的复制通过左值引用可以避免拷贝构造函数的调用,提升性能。对于对象的引用,使用T&类型的左值引用。
    • 实现赋值操作符和拷贝构造函数:左值引用在实现自定义的赋值操作符和拷贝构造函数时是非常重要的。
    • 避免指针的空值检查如果你确信一个对象会在函数调用时存在,使用左值引用比指针更简洁。
class MyClass {
public:MyClass(const MyClass& other) { /* ... */ }  // 拷贝构造函数MyClass& operator=(const MyClass& other) {  // 赋值操作符if (this != &other) { /* ... */ }return *this;}
};
  • 指针(pointer)
    • 动态内存管理:当你需要动态分配内存时,指针是必不可少的。使用newdelete来管理对象的生命周期:
    • 多态性:指针通常用于多态场景(虚函数和继承),尤其是在基类指针指向派生类对象时。
    • 处理空指针(nullptr):指针可以指向nullptr,这意味着你可以检查一个指针是否有效(是否指向一个对象)。这种场景在操作系统编程、网络编程等领域中非常常见。
    • 数组和缓冲区:指针广泛用于处理数组或动态分配的缓冲区(例如字符串或图像数据)。例如,C++中有许多库和低级操作需要直接处理指针。
    • 返回动态分配的内存:指针常用于函数中返回动态分配的内存区域或数组。
class Base {
public:virtual void foo() { std::cout << "Base" << std::endl; }
};class Derived : public Base {
public:void foo() override { std::cout << "Derived" << std::endl; }
};Base* b = new Derived();
b->foo();  // 输出 "Derived"
delete b;
  • 或者
int* createArray(int size) {return new int[size];  // 返回动态分配的内存
}
整合点
  • 左值引用适用于你需要避免拷贝、提供对象的别名、并且对象的生命周期由外部管理的场景。
  • 指针则更适合需要动态内存管理、可能为空的对象、或者涉及多态和数组等低级操作的场景。

2-5 右值引用(rvalue reference)&&
  • 上面的知识点你都看腻了??现在开始才是魔法。
  • 它是一个引用类型,专门用于绑定右值,从而能够在移动语义中实现高效的资源转移。
  • 语法:
T&& ref = obj;  // T 为对象类型,ref 是右值引用,obj 是一个右值对象
  • 特性:
    • 允许绑定右值:右值引用使得右值可以绑定到变量中,通常用于移动资源而不是拷贝。
    • 允许移动语义右值引用使得能够“窃取”资源,避免不必要的深拷贝。
    • 可以将对象转移给其他对象:使用右值引用,可以把一个对象的资源转移给另一个对象,从而避免资源的复制。
  • 我们看一个简单的例子
#include <iostream>int main() {int a = 5;int b = 7;// 绑定右值引用到表达式 a + b 的结果int&& ref = a + b;std::cout << "ref: " << ref << std::endl;  // 输出 12return 0;
}
  • ref 是一个右值引用,绑定到这个临时结果 a + b 上。
  • 关于&&的进一步用法是移动拷贝构造std::move,本文我们后面会说。那才是真正的用法

2-6 引用折叠(Reference Collapsing)
  • 别急,我们很快就要接近移动语义和std::move了,在那之前,我们来看最后一个小知识点。
  • 引用折叠是指当我们将一个类型(通常是右值引用)嵌套在一个模板参数中时,C++ 编译器会自动将多个引用类型合并为一个引用类型。引用折叠的规则使得模板编程变得更加灵活和高效,避免了不必要的引用嵌套。
  • 引用折叠是 C++11 引入的一个特性,它影响到类型推导和函数参数类型的转换。特别是在模板中,引用折叠能够简化右值引用(T&&)和左值引用(T&)的组合。
    • T& & -> T&
    • T& && -> T&
    • T&& & -> T&&
    • T&& && -> T&&
  • 这意味着,如果一个右值引用被传递给一个引用类型,C++ 会根据实际情况自动推导出正确的类型,而不必显式指定类型。
  • 还是一样例子别急,马上一口气会来的。

3 std::move和移动语义

  • 相信聪明的你通过前面介绍的内容或多或少猜到这个函数的作用,嘿嘿
  • 我们来汇总一下前面遗留下来的问题
    1. xvalue的举例
    2. 右值引用的例子
    3. 引用折叠例子
    4. 移动拷贝是个啥
  • 不慌我们一个个来看

3-1 移动语义概念
  • **移动语义(Move Semantics)**是 C++11 引入的一个重要特性,它使得对象的资源管理变得更加高效,特别是在涉及到临时对象或大对象的传递时。==通过移动语义,C++ 可以“移动”对象的资源,而不是进行昂贵的深拷贝。==这样可以减少不必要的内存分配和释放,提高程序的性能。
  • 而移动语义的核心是右值引用(T&&)和 std::move
    • 移动语义的实现:移动构造函数和移动赋值操作符
    • 移动语义的应用:完美转发(Perfect Forwarding)

3-2 std::move
  • std::move 是 C++11 引入的一个标准库函数,位于头文件 <utility>它的主要作用是将一个左值(lvalue)转换为右值(rvalue)引用,从而允许开发者显式地表示一个对象的资源可以被“移动”。这与通常的做法不同,通常我们使用右值引用来表示可以转移的资源,而 std::move 提供了一种机制来执行这种转移。
  • *关键点
    • std::move 并不会“移动”对象本身,它只是将一个左值转化为右值引用,使得对象可以参与“移动语义”。
    • 它是一种类型转换操作,而不是物理上的移动操作。
    • std::move 常用于实现 移动构造函数移动赋值操作符

  • std::move 的语法
std::move(object);
  • object 是一个左值(lvalue),通过 std::move 转换成右值引用(T&&)。
  • std::move 并不会改变对象的内容,它只是将对象转换成右值引用,使得该对象可以被传递给需要右值的函数,通常用于转移资源。
  • 我们看一个例子:
#include <iostream>
#include <utility>  // std::movevoid print(int&& x) {std::cout << "Received rvalue: " << x << std::endl;
}int main() {int a = 10;print(std::move(a));  // 转换左值 'a' 为右值引用// 现在 a 的状态可能无效或被移动std::cout << "a after std::move: " << a << std::endl;  // 输出:a 的值可能不确定return 0;
}
  • std::move(a) 将左值 a 转换为右值引用 int&&,从而允许它被传递给接收右值的函数 print
  • 在调用 std::move(a) 后,a 的值不再保证有效,通常它的值会被设置为不确定或“空”状态。
    * 我知道你肯定想说这东西不如const int & x,这移动语义我不看了(不是)

3-3 知识回顾
  • 在了解std::move的真正用途之前,我们来回顾一点C++基础小知识。
  • 提问:在 C++ 中,每个类都会根据其成员变量和基类的特性自动生成一些特殊的成员函数。他们分别是?
  1. 默认构造函数(ClassName())是一个没有参数的构造函数。如果你没有为类定义任何构造函数,编译器会自动提供一个默认构造函数,前提是类中没有包含常量成员、引用成员,或其他必须初始化的非默认成员。
class MyClass 
{ public:    MyClass(){}}; 
  • 如果类的成员类型不允许默认构造(如类内含有 const 或引用类型的成员),编译器就不会生成默认构造函数,你必须显式地定义它。

  1. 析构函数~ClassName())用于销毁对象并释放其占用的资源。编译器会为你自动生成析构函数,前提是你的类没有显式定义析构函数。自动生成的析构函数会逐个析构类的成员对象。
class MyClass 
{ public:     ~MyClass() { } 
};
  • 如果你的类包含动态分配的内存或者其他需要手动释放的资源(比如文件句柄、网络连接等),你应该自定义析构函数来释放这些资源。

  1. 拷贝构造函数(ClassName(const ClassName&))用于通过另一个同类型对象来初始化一个新对象。如果你没有定义拷贝构造函数,编译器会自动生成一个,它会执行成员逐个拷贝。
class MyClass 
{ public:    int value; MyClass(const MyClass& other){other.value=this->value;}
};  MyClass obj1; 
MyClass obj2 = obj1;  
  • 自动生成的拷贝构造函数是一个 浅拷贝,即逐个拷贝对象的成员变量。如果你的类中有指针或者需要深拷贝的成员,那么你需要自定义拷贝构造函数。

  1. 拷贝赋值操作符ClassName& operator=(const ClassName&))用于将一个对象的值赋给另一个同类型的对象。如果你没有定义拷贝赋值操作符,编译器会自动生成一个,它会执行成员逐个赋值
class MyClass
{// 拷贝赋值操作符MyClass& operator=(const MyClass& other) {if (this == &other)  // 自我赋值检查return *this;return *this;  // 返回当前对象,以支持链式赋值}};
  • 和拷贝构造函数类型,拷贝赋值操作符进行的是==浅拷贝,即逐个拷贝对象的成员变量==。
  • 这里需要注意的是需要自我赋值检查,这行代码的目的是避免自我赋值。在 C++ 中,自我赋值是指一个对象赋值给自己,例如 obj = obj。这种情况通常会引发错误或未定义行为,尤其是在类中涉及动态内存分配或资源管理时,可能会导致资源泄漏、内存损坏等问题。
  • 同时我们注意operator= 的返回值是 *this,即返回当前对象的引用( 左值引用)。这是为了支持链式赋值操作,例如:
MyClass obj1, obj2, obj3;
obj1 = obj2 = obj3;  // 返回左值引用,避免了额外的拷贝或移动
  • 那就这四个吗
    1. 构造函数
    2. 析构函数
    3. 拷贝构造函数
    4. 拷贝赋值操作符
  • 答案是不止,C++11 中引入了两个新的移动构造函数和移动赋值操作符!!!

3-4 移动构造函数和移动赋值操作符
  1. 移动构造函数ClassName(ClassName&&))是在 C++11 中引入的,用于通过“转移”资源的方式构造一个新对象。它避免了不必要的深拷贝,提高了性能,特别是在处理临时对象时。编译器不会自动生成移动构造函数,除非你的类中没有自定义拷贝构造函数、拷贝赋值操作符和析构函数。
class MyClass {
public:int* data;// 移动构造函数MyClass(MyClass&& other) noexcept : data(other.data) {other.data = nullptr;  // 避免原对象在析构时删除资源}
};
  • 如果你的类涉及到动态分配的资源或者拥有大对象,通过移动构造函数可以大大提高性能。在没有显式定义移动构造函数的情况下,编译器将不会自动生成它。
    • noexcept 是一个关键字,用于表示一个函数 不抛出异常。它可以用于函数声明、函数指针以及其他地方的类型标注,表明该函数承诺不会抛出任何异常。使用 noexcept 关键字有助于优化代码的性能,同时使程序更具可预测性和安全性。
  • 编译器仅会自动生成移动构造函数(以及移动赋值操作符)如果以下条件满足:
    • 没有定义拷贝构造函数,且
    • 没有定义拷贝赋值操作符,且
    • 没有定义析构函数
  • 这时,编译器会自动生成一个默认的移动构造函数和移动赋值操作符,通常通过简单地将资源指针从源对象移动到目标对象。

  1. 移动赋值操作符ClassName& operator=(ClassName&&))与移动构造函数类似,是通过将资源从一个临时对象移动到当前对象来优化性能。它可以避免不必要的内存分配和数据复制。
class MyClass {
public:int* data;// 移动赋值操作符MyClass& operator=(MyClass&& other) noexcept {if (this != &other) {delete[] data;  // 释放当前对象的资源data = other.data;  // 转移资源other.data = nullptr;  // 防止源对象析构时释放资源}return *this;}
};
  • 如上面所写的,移动构造函数和移动赋值操作符都接收一个右值引用&&的变量我们来看一个具体的例子。

3-5 测试
  • 我们自定义一个类,分别测试构造函数拷贝构造函数拷贝赋值函数引用移动构造函数移动赋值函数
    • 构造函数MyClass obj1;
    • 拷贝构造函数MyClass obj2(obj1);
    • 拷贝赋值操作符MyClass obj2 = obj1;
    • 引用MyClass& obj2 = obj1;
    • 移动构造函数MyClass obj2 = MyClass(std::move(obj1));
    • 移动赋值操作符MyClass obj2 = std::move(obj1);
  • 这里调用我之前写过的一个时间函数
    • # 基于C++11函数模板实现自动推导返回类型的函数执行时间测量
// 测量函数执行时间的函数
template<typename Func, typename... Args>
double measure_time(Func&& func, Args&&... args)
{// 获取函数开始时间auto start = std::chrono::high_resolution_clock::now();// 调用传入的函数并传递参数std::forward<Func>(func)(std::forward<Args>(args)...);// 获取函数结束时间auto end = std::chrono::high_resolution_clock::now();// 计算执行时间,单位为毫秒std::chrono::duration<double, std::milli> duration = end - start;std::cout<< duration.count()<<"ms"<<std::endl;// 返回函数执行时间(毫秒)return duration.count();
}
  • 但是需要注意的是由于操作系统调度和缓存的影响,单次执行的时间可能会有较大的波动。执行多次测试并计算平均值,可以帮助平滑这些波动,获得更加稳定的测量结果。我们写一个为新的函数
double measure_time_multiple(int num_trials, Func&& func, Args&&... args)
{double total_time = 0.0;for (int i = 0; i < num_trials; ++i) {total_time += measure_time(std::forward<Func>(func), std::forward<Args>(args)...);}return total_time / num_trials;
}
  • 完整测试代码
#include <iostream>
#include <cstdlib>  // std::rand, std::srand
#include <ctime>    // std::time
#include <chrono>   // 用于测量时间
#include <algorithm>  // std::copy
#include <iomanip>    // std::setprecision, std::fixed
#define ARRAY_SIZE  100000class MyClass
{
private:int* data;public:MyClass(){data = new int[ARRAY_SIZE];  // 动态分配内存// 初始化随机数生成器std::srand(static_cast<unsigned>(std::time(0))); // 设置随机种子// 初始化数组为随机数for (int i = 0; i < ARRAY_SIZE; ++i) {data[i] = std::rand() % 100;  // 生成 0 到 99 之间的随机数}}// 拷贝构造函数MyClass(const MyClass& other){data = new int[ARRAY_SIZE];  // 分配新的内存std::copy(other.data, other.data + ARRAY_SIZE, data);  // 复制数据}// 移动构造函数MyClass(MyClass&& other) noexcept : data(other.data){other.data = nullptr;  // 将源对象的数据指针置为空}// 拷贝赋值操作符MyClass& operator=(const MyClass& other){if (this == &other) {return *this;  // 自我赋值检查}// 先释放旧的内存delete[] data;// 分配新的内存并复制数据data = new int[ARRAY_SIZE];std::copy(other.data, other.data + ARRAY_SIZE, data);return *this;}// 移动赋值操作符MyClass& operator=(MyClass&& other) noexcept{if (this == &other) {return *this;  // 自我赋值检查}// 释放当前的内存delete[] data;// 交换数据指针data = other.data;other.data = nullptr;return *this;}// 析构函数~MyClass(){delete[] data;  // 释放动态分配的内存}};// 测量函数执行时间的函数
template<typename Func, typename... Args>
double measure_time(Func&& func, Args&&... args)
{// 获取函数开始时间auto start = std::chrono::high_resolution_clock::now();// 调用传入的函数并传递参数std::forward<Func>(func)(std::forward<Args>(args)...);// 获取函数结束时间auto end = std::chrono::high_resolution_clock::now();// 计算执行时间,单位为毫秒std::chrono::duration<double, std::milli> duration = end - start;return duration.count();
}// 测量函数执行时间的函数
template<typename Func, typename... Args>
double measure_time_multiple(int num_trials, Func&& func, Args&&... args)
{double total_time = 0.0;for (int i = 0; i < num_trials; ++i) {total_time += measure_time(std::forward<Func>(func), std::forward<Args>(args)...);}std::cout << std::fixed << std::setprecision(5) << total_time / num_trials << " ms" << std::endl;return total_time / num_trials;
}int main()
{size_t times=100;std::cout << "构造函数: ";measure_time_multiple(times,[](){MyClass obj1;});std::cout << "拷贝构造函数: ";measure_time_multiple(times,[](){MyClass obj1;MyClass obj2(obj1);});std::cout << "拷贝赋值操作符: ";measure_time_multiple(times,[](){MyClass obj1;MyClass obj2 = obj1;});std::cout << "引用: ";measure_time_multiple(times,[](){MyClass obj1;MyClass& obj2 = obj1;});std::cout << "移动构造函数: ";measure_time_multiple(times,[](){MyClass obj1;MyClass obj2 = MyClass(std::move(obj1));});std::cout << "移动赋值操作符: ";measure_time_multiple(times,[](){MyClass obj1;MyClass obj2 = std::move(obj1);  // 移动赋值});return 0;
}
  • 我们来看看平均测试100次,的平均时间请添加图片描述

  • 很直观的有数值的区别,我们甚至可以加大测试数据的大小请添加图片描述

  • 可以看到引用,移动构造函数,移动赋值操作符三者没有进行数据拷贝,可以迅速的进行。


3-6 左值引用和移动语义
  • 那么你可能会这样的一个疑问,下面这两种看起来完全可以替换
class MyClass
{};
void func(MyClass&& myclass)
{}
void func(MyClass& myclass)
{}MyClass myclass;func(myclass);
func(std::move(myclass));
  • 我们来解决之前遗留下来的问题,std::move(obj1) 其实是一个 xvalue,它把 obj1 转换成了一个右值,允许 obj2 通过 移动构造函数obj1 获取资源。
  • 在执行完std::move后通过会调用类内部的移动拷贝或者移动赋值函数,通常意味着原本的数据将不再被使用(可能被设为nullptr)
  • 因此就诞生了这两种的区别
3-6-1 void func(MyClass& myclass); // 左值引用
    • 左值引用 绑定到 左值,即已存在的对象。它不能绑定到 右值,除非通过 std::move 显式地将一个左值转换为右值(xvalue)。
  • 通过左值引用传递的对象仍然有效,原对象的生命周期没有结束,因此你可以继续使用它。
  • 通常用于传递对象并可能进行修改,或者在不需要拷贝的情况下传递大对象。
3-6-2 void func(MyClass&& myclass); // 右值引用
  • 右值引用 绑定到 右值,例如临时对象、字面量、返回值等。右值引用的作用是通过 移动语义 来转移资源,而不是拷贝它们。
  • 当你传递一个右值引用时,原对象的资源可能会被移动,并且原对象进入一种“已废弃”或“无效”状态(通常会将指针设为 nullptr,或者状态被修改以表示它不能再被使用)。
  • 右值引用 常常用于 移动构造函数移动赋值操作符 中,这样你就可以避免不必要的资源拷贝,提升性能。

3-7 引用折叠例子
  • 我们来看看之前的几个遗留下来的问题
    1. xvalue的举例(讲完啦)
    2. 右值引用的例子(讲完啦)
    3. 引用折叠例子
    4. 移动拷贝是个啥(讲完啦)
  • 我们直接来看一个例子:
#include <iostream>
#include <type_traits>  // std::is_sametemplate <typename T>
void print(T&& arg) {std::cout << "Received argument of type: " << typeid(arg).name() << std::endl;
}int main() {int x = 5;// 传递左值引用(T&)print(x);  // T&& 会折叠为 T&,所以 arg 的类型是 int&// 传递右值引用(T&&)print(std::move(x));  // T&& 会保持为 T&&,所以 arg 的类型是 int&&return 0;
}

5 完美转发和万能引用(真正&&使用用途)

  • 完美转发(Perfect Forwarding)是 C++11 引入的一种技术,它允许你将函数的参数转发给其他函数时,不丢失参数的值类别(左值或右值)。通过完美转发,你可以确保被转发的参数以正确的值类别传递给目标函数,从而避免不必要的拷贝或移动操作。
  • 完美转发通常与 右值引用std::forward 配合使用,确保参数在传递给下游函数时以正确的形式(左值或右值)传递
5-1 万能引用
  • 万能引用(Forwarding Reference),是 C++11 引入的一种特殊的引用类型,通常出现在模板中,表现为 T&&,其中 T 是模板类型参数。它能够根据传入的值(左值或右值)自动调整自己的行为,是实现 完美转发(Perfect Forwarding)的一种重要工具。
  • 在模板函数中,==T&& 被称为 万能引用。这个术语其实是指模板函数中的右值引用,具有根据传入的参数是左值还是右值来决定是否绑定为左值引用或右值引用的特性。==对于 万能引用,其行为取决于传入的参数:
    • 如果传入的是左值T&& 会展开为左值引用,即 T&
    • 如果传入的是右值T&& 会保持为右值引用,即 T&&
template <typename T>
void func(T&& arg) { std::cout << "传递的是左值" << std::endl; 
}

5-2 完美转发的场景
  • 假设你有一个函数 func,它接受一个参数,并将该参数转发给另一个函数 targetFunc。你希望确保传递给 targetFunc 的参数能保持其原始的值类别(即如果它是一个右值,就转发为右值;如果它是左值,则转发为左值)。
  • 我们尝试不使用std::forward进行参数传递
#include <iostream>
#include <utility>  // std::moveclass MyClass {};// 目标函数:接受左值和右值
void targetFunc(MyClass& myclass) {std::cout << "左值版本: " << std::endl;}void targetFunc(MyClass&& myclass) {std::cout << "右值版本: " << std::endl;}// 完美转发函数(没有使用 std::forward)
template <typename T>
void imperfectForward(T&& arg) {targetFunc(arg);  
}int main() {MyClass myclass;  // myclass 是一个左值std::cout << "调用 targetFunc(myclass):" << std::endl;imperfectForward(myclass);  // 传递左值std::cout << "调用 targetFunc(std::move(myclass)):" << std::endl;imperfectForward(std::move(myclass));  // 传递右值return 0;
}
  • 我们来看看输出请添加图片描述

  • 可以看到std::move(myclass)被判定为左值了,这就是不调用std::forward的后果

  • std::forward<T>(arg) 的作用:
    • 如果 T 是左值引用类型std::forward<T>(arg) 将保持 arg 为左值引用。
    • 如果 T 不是左值引用std::forward<T>(arg) 将把 arg 转发为右值。
  • 我们改进代码
// 完美转发函数(使用 std::forward)
template <typename T>
void imperfectForward(T&& arg) {targetFunc(std::forward<T>(arg));  
}

请添加图片描述


5-3 进一步解析—为什么需要 std::forward
  • 那你可能会问,既然这样,我直接使用左值引用传递不久可以解决问题了吗,那么你来看看这个
#include <iostream>
#include <string>class Person {
public:Person(const std::string& n, int a) : name(n), age(a) {std::cout << "Constructor: " << name << ", " << age << std::endl;}// 拷贝构造函数Person(const Person& other) : name(other.name), age(other.age) {std::cout << "Copy Constructor: " << name << ", " << age << std::endl;}// 移动构造函数Person(Person&& other) noexcept : name(std::move(other.name)), age(other.age) {std::cout << "Move Constructor: " << name << ", " << age << std::endl;}void print() const {std::cout << "Person: " << name << ", " << age << std::endl;}private:std::string name;int age;
};// 使用左值引用传递参数
template <typename T>
void introduce_person(T& person) {  // 这里 T& 是左值引用std::cout << "Introducing person: ";person.print();
}int main() {Person p1("Alice", 30);  // 创建 Person 对象,调用构造函数std::cout << "\nPassing left value:\n";introduce_person(p1);  // 传递左值std::cout << "\nPassing right value (this will not compile with T&):\n";introduce_person(Person("Bob", 25));  // 传递右值,这会编译错误return 0;
}

请添加图片描述

  • 如你所见,这是一个致命的错误,左值引用无法绑定右值引用!!!!!!(这看起来是废话但是在这里我们就会报错了)
  • 我们引入std::forward就可以完美应对无论是左值还是右值,而且还能保持性质不改变!!!
// 使用完美转发
template <typename T>
void introduce_person_forward(T&& person) {std::cout << "Introducing person (with forwarding): ";T new_person = std::forward<T>(person);  // 完美转发new_person.print();
}

请添加图片描述


6 总结

  • 本节我们讲解了左值右值,引用,引用折叠,移动语义,万能引用与完美转发
  • 下一节我们讲讲智能指针完美转发的配合
  • 如有错误,欢迎指出!!!!
  • 感谢大家的支持!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/64381.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

暂停一下,给Next.js项目配置一下ESLint(Next+tailwind项目)

前提 之前开自己的GitHub项目&#xff0c;想着不是团队项目&#xff0c;偷懒没有配置eslint&#xff0c;后面发现还是不行。eslint的存在可以帮助我们规范代码格式&#xff0c;同时 ctrl s保存立即调整代码格式是真的很爽。 除此之外&#xff0c;团队使用eslint也是好处颇多…

iOS 应用的生命周期

Managing your app’s life cycle | Apple Developer Documentation Performance and metrics | Apple Developer Documentation iOS 应用的生命周期状态是理解应用如何在不同状态下运行和管理资源的基础。在 iOS 开发中&#xff0c;应用生命周期管理的是应用从启动到终止的整…

Hadoop学习笔记(包括hadoop3.4.0集群安装)(黑马)

Hadoop学习笔记 0-前置章节-环境准备 0.1 环境介绍 配置环境&#xff1a;hadoop-3.4.0&#xff0c;jdk-8u171-linux-x64 0.2 VMware准备Linux虚拟机 0.2.1主机名、IP、SSH免密登录 1.配置固定IP地址&#xff08;root权限&#xff09; 开启master&#xff0c;修改主机名为…

扩展SpringBoot中的SpringMVC的默认配置

SpringBoot默认已经给我们做了很多SpringMVC的配置&#xff0c;哪些配置&#xff1f; 视图解析器ViewResolver静态资料的目录默认首页index.html图标名字和图标所在目录&#xff0c;favicon.ico类型转换器Converter&#xff0c;格式转换器的Formatter消息转换器HttpMessageCon…

企业内训|阅读行业产品运营实战训练营-某运营商数字娱乐公司

近日&#xff0c;TsingtaoAI公司为某运营商旗下数字娱乐公司组织的“阅读行业产品运营实战训练营”在杭州落下帷幕。此次训练营由TsingtaoAI资深互联网产品专家程靖主持。该公司的业务骨干——来自内容、市场、业务、产品与技术等跨部门核心岗位、拥有8-10年实战经验的中坚力量…

Android Room 数据库使用详解

一、Room介绍 Android Room 是 Google 提供的一个 Android 数据持久化库&#xff0c;是 Android Jetpack 组成部分之一。它提供了一个抽象层&#xff0c;使得 SQLite 数据库的使用更为便捷。通过 Room&#xff0c;开发者可以轻松地操作数据库&#xff0c;不需要直接编写繁琐的…

双目测距中的鼠标操作回调函数

参考&#xff1a;【OpenCV】双目测距&#xff08;双目标定、双目校正和立体匹配&#xff09; /*****描述&#xff1a;鼠标操作回调函数定义*****/ static void onMouse(int event, int x, int y, int, void*) {if (selectObject){selection.x MIN(x, origin.x);selection.y …

Kaggler日志--Day7

进度24/12/17 昨日复盘&#xff1a; 尝试自己爬取了两个学校的就业信息数据&#xff0c;比较简单但是顺通了爬虫流程 看别人的代码&#xff1a;AQX的。 今日进度&#xff1a; 分析理解昨天代码的过程&#xff0c;统计问题 过程理解 EDA部分 对于不同变量类型判别的举例说明…

NDRCContextUnmarshall断点函数分析之I_RpcBindingCopy函数的作用

NDRCContextUnmarshall断点函数分析之I_RpcBindingCopy函数的作用 第一部分&#xff1a; void RPC_ENTRY NDRCContextUnmarshall ( // process returned context OUT NDR_CCONTEXT PAPI *phCContext,// stub context to update IN RPC_BINDING_HANDLE hRPC, …

IS-IS协议

IS-IS协议介绍 IS-IS&#xff08;Intermediate System to Intermediate System&#xff09;协议是一种链路状态的内部网关协议&#xff08;IGP&#xff09;&#xff0c;用于在同一个自治系统&#xff08;Autonomous System, AS&#xff09;内部的路由器之间交换路由信息。IS-I…

QoS分类和标记

https://zhuanlan.zhihu.com/p/160937314 1111111 分类和标记是识别每个数据包优先级的过程。 这是QoS控制的第一步&#xff0c;应在源主机附近完成。 分组通常通过其分组报头来分类。下图指定的规则仔细检查了数据包头 &#xff1a; 下表列出了分类标准&#xff1a; 普通二…

电机控制杂谈(23)——共模电压与轴电流

1.共模电压与轴电流的关系和危害 对于电压源换流器&#xff0c;由于功率半导体器件的快速开关和PWM调制方案&#xff0c;将在电机定子绕组的中性点&#xff08;N&#xff09;和接地点&#xff08;O&#xff09;之间产生高频共模电压&#xff08;Common-mode voltage&#xff0…

FPGA设计-使用 lspci 和 setpci 调试xilinx的PCIe 问题

目录 简介 lspci lspci-TV lspci-vvv 注意事项 lspci -vs lspci -vvvs 设置pci 识别setpci中的寄存器 setpci -s 00:01.0 d0.b42 简介 lspci 和 setpci 命令在 Linux 发行版中本身可用。该命令具有各种级别的输出&#xff0c;并提供非常有用的时间点查看 PCI 总线…

vue+node+mysql8.0,详细步骤及报错解决方案

1.下载需要安装的插件 下载express npm install express下载cors&#xff0c;用于处理接口跨域问题 npm install cors下载mysql npm install mysql 2.配置服务器 可以在vue项目的src同级创建server文件夹&#xff08;这里的位置可随意选择&#xff09; 然后依次创建&#…

【人工智能】因果推断与数据分析:用Python探索数据间的因果关系

解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 因果推断是数据科学领域的一个重要方向,旨在发现变量间的因果关系,而不仅仅是相关性。本篇文章将从因果推断的理论基础出发,介绍因果关系的定义与建模方法,涵盖因果图(Causal Graph)、d-分离、反事实估计等…

并发修改导致MVCC脏写问题

并发修改导致MVCC脏写问题 一、概要 1.1 业务场景 数据库表结构设计&#xff1a; 一个主档数据&#xff0c;通过一个字段&#xff0c;逗号分隔的方式去关联其他明细信息的id。 如主档数据A&#xff0c;有3条明细数据与A关联&#xff0c;其id分别是1,2,3&#xff0c;那么其存…

[创业之路-198]:华为的成立发展与新中国的建立与发展路径的相似性比较

目录 一、公司比较 1、创业初期的艰难与挑战 2、坚持自主创新与研发 3、市场拓展与国际化战略 4、企业文化与社会责任 5、面临的挑战与应对策略 二、任正非管理企业的思想大量借鉴了毛泽东建国的思想 1、矛盾论与企业管理 2、群众路线与企业文化 3、战略思维与长远发…

深入解析与示例:ROS中的catkin_make构建过程

深入解析与示例&#xff1a;ROS中的catkin_make构建过程 catkin_make 是用于构建ROS&#xff08;Robot Operating System&#xff09;中的catkin软件包的命令行工具。它的主要功能是编译工作空间中所有catkin软件包&#xff0c;并确保按照依赖关系正确构建每个软件包。下面详细…

PugiXML,一个高效且简单的 C++ XML 解析库!

嗨&#xff0c;大家好&#xff01;我是一行。今天要给大家介绍 PugiXML&#xff0c;这可是 C 里处理 XML 数据的得力助手。它能轻松地读取、修改和写入 XML 文件&#xff0c;就像一个专业的 XML 小管家&#xff0c;不管是解析配置文件&#xff0c;还是处理网页数据&#xff0c;…

SSE(Server-Sent Events)主动推送消息

说明 使用Java开发web应用&#xff0c;大多数时候我们提供的接口返回数据都是一次性完整返回。有些时候&#xff0c;我们也需要提供流式接口持续写出数据&#xff0c;以下提供一种简单的方式。 SSE&#xff08;Server-Sent Events&#xff09; SSE 是一种允许服务器单向发送事…