当前位置: 首页 > news >正文

C++11 ——右值引用和移动语义

目录

  • 一、基本概念
    • 左值 vs 右值
    • 左值引用 vs 右值引用
  • 二、右值引用使用场景和意义
    • 左值引用的使用场景
    • 左值引用的短板
    • 右值引用和移动语义
    • 右值引用引用左值
    • 右值引用的其他使用场景
    • 右值引用总结
  • 三、完美转发
    • 右值前置知识
    • 万能引用
    • 完美转发保持值的属性
    • 完美转发的使用场景
  • 四、总结


一、基本概念

左值 vs 右值

左值

左值是一个表示数据的表达式,如变量名或解引用的指针,左值可以被取地址,也可以被修改(const修饰的左值除外),左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。

int main()
{//以下的p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;const char (*p)[3] = &"42";  // 合法,取字符串字面量的地址// char* q = "42";           // 错误(C++11起):字符串字面量是const的const char* r = "42";        // 合法,退化为const char*return 0;
}

右值

右值也是一个表示数据的表达式,如字母常量、表达式的返回值、函数的返回值(不能是左值引用返回)等等,右值不能被取地址,也不能被修改,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边。

int main()
{double x = 1.1, y = 2.2;//以下几个都是常见的右值10;x + y;fmin(x, y);//错误示例(右值不能出现在赋值符号的左边)//10 = 1;//x + y = 1;//fmin(x, y) = 1;return 0;
}
  • 右值本质就是一个临时变量或常量值,比如代码中的10就是常量值,表达式x+y和函数fmin的返回值就是临时变量,这些都叫做右值。
  • 这些临时变量和常量值并没有被实际存储起来,这也就是为什么右值不能被取地址的原因,因为只有被存储起来后才有地址。
  • 但需要注意的是,这里说函数的返回值是右值,指的是传值返回的函数,因为传值返回的函数在返回对象时返回的是对象的拷贝,这个拷贝出来的对象就是一个临时变量。

简单记忆:左值像“容器”特例字符串常量,右值像“内容”。

左值引用 vs 右值引用

传统的C++语法中就有引用的语法,而C++11中新增了右值引用的语法特性,为了进行区分,于是将C++11之前的引用就叫做左值引用。但是无论左值引用还是右值引用,本质都是给对象取别名。

左值引用
右值引用使用场景和意义
左值引用就是对左值的引用,给左值取别名,通过“&”来声明。

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;
}

右值引用

右值引用就是对右值的引用,给右值取别名,通过“&&”来声明。

int main()
{double x = 1.1, y = 2.2;//以下几个都是常见的右值10;x + y;fmin(x, y);//以下几个都是对右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double rr3 = fmin(x, y);return 0;
}

需要注意的是,右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,这时这个右值可以被取到地址,并且可以被修改,如果不想让被引用的右值被修改,就用const修饰右值引用。

int main()
{double x = 1.1, y = 2.2;int&& rr1 = 10;const double&& rr2 = x + y;rr1 = 20;rr2 = 5.5; //报错return 0;
}

左值引用是否可以引用右值

  • 左值引用不能引用右值,因为这涉及权限放大的问题,右值是不能被修改的,而左值引用是可以修改。
  • 但是const左值引用可以引用右值,因为const左值引用能够保证被引用的数据不会被修改。

因此const左值引用既可以引用左值,也可以引用右值。

template<class T>
void func(const T& val)
{cout << val << endl;
}
int main()
{string s("hello");func(s);       //s为左值func("world"); //"world"为右值return 0;
}

右值引用是否可以引用左值

  • 右值引用只能引用右值,不能引用左值。
  • 但是右值引用可以引用move以后的左值。

move函数是C++11标准提供的一个函数,被move后的左值能够赋值给右值引用。

int main()
{int a = 10;//int&& r1 = a;     //右值引用不能引用左值int&& r2 = move(a); //右值引用可以引用move以后的左值return 0;
}

二、右值引用使用场景和意义

虽然const左值引用既能接收左值,又能接收右值,但左值引用终究存在短板,而C++11提出的右值引用就是用来解决左值引用的短板的,例如字符串的深拷贝,因为临时变量出了作用域就销毁,只能传值返回而不能传引用返回。

为了更好的说明问题,这里需要借助一个深拷贝的类,下面模拟实现了一个简化版的string类。类当中实现了一些基本的成员函数,并在string的拷贝构造函数和赋值运算符重载函数当中打印了一条提示语句就能够知道调用这两函数了。

namespace li
{class string{public:typedef char* iterator;iterator begin(){return _str; //返回字符串中第一个字符的地址}iterator end(){return _str + _size; //返回字符串中最后一个字符的后一个字符的地址}string(const char* str = "")			//构造函数{_size = strlen(str); //初始时,字符串大小设置为字符串长度_capacity = _size; //初始时,字符串容量设置为字符串长度_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')strcpy(_str, str); //将C字符串拷贝到已开好的空间}void swap(string& s){//调用库里的swap::swap(_str, s._str); //交换两个对象的C字符串::swap(_size, s._size); //交换两个对象的大小::swap(_capacity, s._capacity); //交换两个对象的容量}string(const string& s)					//拷贝构造函数(现代写法):_str(nullptr), _size(0), _capacity(0){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象swap(tmp); //交换这两个对象}string& operator=(const string& s)		//赋值运算符重载(现代写法){cout << "string& operator=(const string& s) -- 深拷贝" << endl;string tmp(s); //用s拷贝构造出对象tmpswap(tmp); //交换这两个对象return *this; //返回左值(支持连续赋值)}~string(){delete[] _str;  //释放_str指向的空间_str = nullptr; //及时置空,防止非法访问_size = 0;      //大小置0_capacity = 0;  //容量置0}char& operator[](size_t i)				//[]运算符重载{assert(i < _size); //检测下标的合法性return _str[i]; //返回对应字符}void reserve(size_t n)					//仅扩容{if (n > _capacity) //当n大于对象当前容量时才需执行操作{char* tmp = new char[n + 1]; //多开一个空间用于存放'\0'strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')delete[] _str; //释放对象原本的空间_str = tmp; //将新开辟的空间交给_str_capacity = n; //容量跟着改变}}void push_back(char ch){if (_size == _capacity) //判断是否需要增容{reserve(_capacity == 0 ? 4 : _capacity * 2); //将容量扩大为原来的两倍}_str[_size] = ch; //将字符尾插到字符串_str[_size + 1] = '\0'; //字符串后面放上'\0'_size++; //字符串的大小加一}string& operator+=(char ch)			//返回C类型的字符串{push_back(ch); //尾插字符串return *this; //返回左值(支持连续+=)}const char* c_str()const			//返回C类型的字符串{return _str;}private:size_t _size;size_t _capacity;char* _str;};
}

如下是左值引用和右值引用的优先匹配版本。

#include <iostream>
using namespace std;//构成函数重载
void func(int& r)
{cout << "void func(const int& r)" << endl;
}void func(int&& r)
{cout << "void func(int&& r)" << endl;
}int main()
{int a = 1, b = 2;//本身是左值,匹配左值引用func(a);//本身是右值,匹配右值引用,若没有右值引用函数则可以匹配左值引用因为左值引用加const了func(a + b);//它们本质是优先匹配最合适的版本,若没有对应的左值引用或右值引用,则匹配符合语法的也行return 0;
}

左值引用的使用场景

在说明左值引用的短板之前,先来看看左值引用的使用场景:

  • 左值引用做参数,防止传参时进行拷贝操作。
  • 左值引用做返回值,防止返回时对返回对象进行拷贝操作。
  • 都减少了拷贝的这一环节,提高了效率方面。
//构造函数重载
void func1(li::string s)
{}
void func2(const cl::string& s)
{}
int main()
{li::string s("hello world");func1(s);  //值传参func2(s);  //左值引用传参s += 'X';  //左值引用返回return 0;
}

左值引用的短板

左值引用虽然能避免不必要的拷贝操作,但左值引用并不能完全避免。

  • 左值引用做参数,能够完全避免传参时不必要的拷贝操作。
  • 左值引用做返回值,并不能完全避免函数返回对象时不必要的拷贝操作。

如果函数返回的对象是一个局部变量,该变量出了函数作用域就被销毁了,这种情况下不能用左值引用作为返回值,只能以传值的方式返回,这就是左值引用的短板。

比如下面模拟实现一个整形转字符串的函数,这个to_string函数就不能使用左值引用返回,因为to_string函数返回的是一个局部变量。

namespace li
{li::string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}li::string str;while (value > 0){int x = value % 10;value /= 10;str += (x + '0');}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;}
}

调用to_string函数返回时,就会调用string的拷贝构造函数。

int main()
{li::string s = li::to_string(1234);return 0;
}

C++11提出右值引用解决左值引用的这个短板。

右值引用和移动语义

右值引用和移动语句解决上述问题的方式就是,给当前模拟实现的string类增加移动构造和移动赋值方法。

移动构造

移动构造是个构造函数,该构造函数的参数是右值引用类型的,移动构造本质就是将传入右值的资源窃取过来,占为己有,这样就避免了进行深拷贝,所以它叫做移动构造,就是窃取别人的资源来构造自己的意思。

在当前的string类中增加一个移动构造函数,该函数要做的就是调用swap函数将传入右值的资源窃取过来,为了能够更好的得知移动构造函数是否被调用,可以在该函数当中打印一条提示语句。

核心作用

  • 资源窃取:直接“夺取”另一个对象的资源(如内存、文件句柄等),避免深拷贝的开销。

  • 性能优化:适用于临时对象或显式标记为可移动的对象(如 std::move 后的对象)。

namespace li {class string {public:// 移动构造函数string(string&& s) : _str(s._str), _size(s._size), _capacity(s._capacity) 		//直接窃取资源{cout << "string(string&& s) -- 移动构造" << endl;s._str = nullptr;  // 置空原对象s._size = 0;s._capacity = 0;}private:size_t _size;size_t _capacity;char* _str;};
}

避免在移动构造中用 swap

  • 性能:swap 需要两次赋值(当前对象初始化 + 交换),而直接窃取只需一次。

移动构造和拷贝构造的区别:

  • 在没有增加移动构造之前,由于拷贝构造采用的是const左值引用接收参数,因此无论拷贝构造对象时传入的是左值还是右值,都会调用拷贝构造函数。
  • 增加移动构造之后,由于移动构造采用的是右值引用接收参数,因此如果拷贝构造对象时传入的是右值,那么就会调用移动构造函数(最匹配原则)。
  • string的拷贝构造函数做的是深拷贝,而移动构造函数做的是浅拷贝中只需要调用swap函数进行资源的转移,因此调用移动构造的代价比调用拷贝构造的代价小。

简单理解为:拷贝构造给左值(const T&)使用,移动构造给右值(T&&)使用,包括纯右值和将亡值。
如果只有拷贝构造,右值也会调用拷贝(但可能不高效),如果同时存在拷贝和移动构造,右值会优先匹配移动构造。

给string类增加移动构造后,对于返回局部string对象的这类函数,在返回string对象时就会调用移动构造进行资源的移动,而不会再调用拷贝构造函数进行深拷贝了。

int main()
{li::string ret = li::func();return 0;
}

说明:

  • 虽然to_string当中返回的局部string对象是一个左值,但由于该string对象在当前函数调用结束后就会立即被销毁,我可以把这种即将被消耗的值叫做“将亡值”,比如匿名对象也可以叫做“将亡值”。
  • 既然“将亡值”马上就要被销毁了,那还不如把它的资源转移给别人用,因此编译器在识别这种“将亡值”时会将其识别为右值,这样就可以匹配到参数类型为右值引用的移动构造函数。

编译器做的优化

当一个函数在返回局部对象时,会先用这个局部对象拷贝构造出一个临时对象,然后再用这个临时对象来拷贝构造我们接收返回值的对象。
在这里插入图片描述

在C++11标准出来之前,对于深拷贝的类来说这里就会进行两次深拷贝,所以大部分编译器为了提高效率都对这种情况进行了优化,这种连续调用构造函数的场景通常会被优化成一次。
在这里插入图片描述
在C++11标准出来之前这里应该调用两次string的拷贝构造函数,但最终被编译器优化成了一次,减少了一次无意义的深拷贝。(并不是所有的编译器都做了这个优化)

在C++11出来之后,编译器的这个优化仍然起到了作用。

  • 如果编译器不优化这里应该调用两次移动构造,第一次调用移动构造用返回的局部string对象构造出一个临时对象,第二次调用移动构造用这个临时对象构造接收返回值的对象。
  • 而经过编译器优化后,最终这两次移动构造就被优化成了一次,也就是直接将返回的局部string对象的资源移动给了接收返回值的对象。
  • 此外,C++11之后就算编译器没有进行这个优化问题也不大,因为不优化也就是调用两次移动构造进行两次资源的转移。

如果不是用函数的返回值来构造一个对象,而是用一个之前已经定义出来的对象来接收函数的返回值,这时编译器就无法进行优化了。
在这里插入图片描述
这时当函数返回局部对象时,会先用这个局部对象拷贝构造出一个临时对象,然后再调用赋值运算符重载函数将这个临时对象赋值给接收函数返回值的对象。

  • 编译器并没有对这种情况进行优化,因此在C++11标准出来之前,对于深拷贝的类来说这里就会存在两次深拷贝,因为深拷贝的类的赋值运算符重载函数也需要以深拷贝的方式实现。
  • 但在深拷贝的类中引入C++11的移动构造后,这里仍然需要再调用一次赋值运算符重载函数进行深拷贝,因此深拷贝的类不仅需要实现移动构造,还需要实现移动赋值。

对于返回局部对象的函数,就算只是调用函数而不接收该函数的返回值,也会存在一次拷贝构造或移动构造,因为函数的返回值不管你接不接收都必须要有,而当函数结束后该函数内的局部对象都会被销毁,所以就算不接收函数的返回值也会调用一次拷贝构造或移动构造生成临时对象。

移动赋值

移动赋值是一个赋值运算符重载函数,该函数的参数是右值引用类型;移动赋值就是 “资源抢劫”;把一个临时对象(或明确不要的对象)的资源(比如内存、文件句柄)直接占为己有,避免重新分配和复制的开销。

简单演示一个例子。

class string {
public:// 移动赋值操作string& operator=(String&& other) {if (this != &other) {      // 防止自己抢自己delete[] data;        // 丢掉自己的旧资源data = other.data; 	  // 直接抢别人的资源other.data = nullptr; // 把别人的资源置空}return *this;}
private:char* data; 				// 资源
};

在string类中增加一个移动赋值函数,该函数要做的就是调用swap函数将传入右值的资源窃取过来,为了能够更好的得知移动赋值函数是否被调用,可以在该函数中打印一条提示语句。

namespace li
{class string{public://移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}private:size_t _size;size_t _capacity;char* _str;};
}

移动赋值和原有operator=函数的区别:

  • 在没有增加移动赋值之前,由于原有operator=函数采用的是const左值引用接收参数,因此无论赋值时传入的是左值还是右值,都会调用原有的operator=函数。
  • 增加移动赋值之后,由于移动赋值采用的是右值引用接收参数,因此如果赋值时传入的是右值,那么就会调用移动赋值函数(最匹配原则)。
  • string原有的operator=函数做的是深拷贝,而移动赋值函数中只需要调用swap函数进行资源的转移,因此调用移动赋值的代价比调用原有operator=的代价小。

现在给string增加移动构造和移动赋值以后,就算是用一个已经定义过的string对象去接收to_string函数的返回值,此时也不会存在深拷贝。

int main()
{li::string s;//...s = li::to_string(1234);return 0;
}

此时当to_string函数返回局部的string对象时,会先调用移动构造生成一个临时对象,然后再调用移动赋值将临时对象的资源转移给接收返回值的对象,这个过程虽然调用了两个函数,但这两个函数要做的只是资源的移动,而不需要进行深拷贝,大大提高了效率。

STL中的容器

C++11标准出来之后,STL中的容器都增加了移动构造和移动赋值。
例如string类的移动构造。
在这里插入图片描述
string类的移动赋值。
在这里插入图片描述

右值引用引用左值

右值引用不能直接引用左值,通过move函数实现对左值引用转为右值引用,将一个左值强制转化为右值引用的功能,实现移动语义。

move函数的定义。

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{//forward _Arg as movablereturn ((typename remove_reference<_Ty>::type&&)_Arg);
}

说明:

  • move函数中_Arg参数的类型不是右值引用,而是万能引用。万能引用跟右值引用的形式一样,但是右值引用需要是确定的类型。
  • 一个左值被move以后,它的资源可能就被转移给别人了,因此要慎用一个被move后的左值。

右值引用的其他使用场景

右值引用版本的插入函数

C++11标准出来之后,STL中的容器除了增加移动构造和移动赋值之外,STL容器插入接口函数也增加了右值引用版本。
在这里插入图片描述

右值引用版本插入函数的意义

若vector容器当中存储的是string对象,那么在调用push_back向vector容器中插入元素时,可能会有如下几种插入方式。

int main()
{list<li::string> lt;li::string s("1111");lt.push_back(s); 				  //调用string的拷贝构造lt.push_back(li::string("2222")); //调用string的移动构造lt.push_back(std::move(s));       //调用string的移动构造//尽管匿名对象3333是纯右值,隐式触发移动语义lt.push_back("3333");             //调用string的移动构造return 0;
}

list容器的push_back函数需要先构造一个结点,然后将该结点插入到底层的双链表当中。

  • 在C++11之前list容器的push_back接口只有一个左值引用版本,因此在push_back函数中构造结点时,这个左值只能匹配到string的拷贝构造函数进行深拷贝。
  • 在C++11出来之后,string类提供了移动构造函数,并且list容器的push_back接口提供了右值引用版本,此时如果传入push_back函数的string对象是一个右值,那么在push_back函数中构造结点时,这个右值就可以匹配到string的移动构造函数进行资源的转移,这样就避免了深拷贝,提高了效率。
  • 上述代码中的插入第一个元素时就会匹配到push_back的左值引用版本,在push_back函数内部就会调用string的拷贝构造函数进行深拷贝,而插入后面三个元素时由于传入的是右值,因此会匹配到push_back的右值引用版本,此时在push_back函数内部就会调用string的移动构造函数进行资源的转移。

右值引用总结

右值分为两种:
纯右值(prvalue):如 42, std::string(“hello”), x + y
将亡值(xvalue):如 std::move(x), 返回右值引用的函数调用

当说"纯右值被转换为右值"时,实际上是指:
纯右值在特定上下文(如函数参数传递)中会被实质化(materialized)为一个临时对象
这个临时对象属于将亡值(xvalue),可以绑定到右值引用(T&&)

//字符串字面量
auto p = &"hello";  // 合法,"hello"是左值
const char (*ptr)[6] = &"hello"; // 取地址合法//初始化std::string
std::string s = "hello"; 
// 发生:
// 1. "hello"(左值)触发std::string的构造函数
// 2. 构造临时std::string对象(这个临时对象是右值)
  • std::move 的本质:将左值显式转换为右值

std::move 的作用就是显式将一个左值强制转换为右值引用(T&&)不复制、不移动数据,从而允许移动语义(如移动构造或移动赋值)被调用。它本身不执行任何实际的“移动”操作,只是标记该对象可以被移动。

三、完美转发

右值前置知识

右值引用变量是左值

当一个右值引用被赋予名字后,它就变成了一个左值(可以取地址,有持久状态):

void foo(int&& x) {  // x是右值引用,但有名字int* p = &x;     // 合法!x是左值x = 42;          // 可以修改,x是左值
}int main() {foo(10);  		// 10是右值,但进入foo后x是左值
}

x 是函数参数,有名字,所以是左值,虽然它的类型是 int&&(右值引用),但变量本身是左值。

右值引用类型绑定右值

右值引用类型(T&&)专门用于绑定到右值:

int&& rref1 = 10;  // 右值引用绑定到右值
// int&& rref2 = rref1; // 错误!rref是左值,不能绑定到右值引用

万能引用

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

template<class T>
void PerfectForward(T&& t)
{//...
}

右值引用和万能引用的区别就是,右值引用需要是确定的类型,而万能引用是根据传入实参的类型进行推导,需要有模板;如果传入的实参是一个左值,那么保留原类型的形参t就是左值引用,如果传入的实参是一个右值,那么这里的形参t就是右值引用。

重载了四个Func函数,这四个Func函数的参数类型分别是左值引用、const左值引用、右值引用和const右值引用。在主函数中调用PerfectForward函数时分别传入左值、右值、const左值和const右值,在PerfectForward函数中再调用Func函数。

void Func(int& x)
{cout << "左值引用" << endl;
}
void Func(const int& x)
{cout << "const 左值引用" << endl;
}
void Func(int&& x)
{cout << "右值引用" << endl;
}
void Func(const int&& x)
{cout << "const 右值引用" << endl;
}
template<typename T>
void PerfectForward(T&& t)
{Func(t);
}
int main()
{int a = 10;PerfectForward(a);       //左值PerfectForward(move(a)); //右值const int b = 20;PerfectForward(b);       //const 左值PerfectForward(move(b)); //const 右值return 0;
}

运行如下:
在这里插入图片描述

由于PerfectForward函数的参数类型是万能引用,因此既可以接收左值也可以接收右值,而我们在PerfectForward函数中调用Func函数,就是希望调用PerfectForward函数时传入左值、右值、const左值、const右值,能够匹配到对应版本的Func函数。

  • 但实际调用PerfectForward函数时传入左值和右值,最终都匹配到了左值引用版本的Func函数,调用PerfectForward函数时传入const左值和const右值,最终都匹配到了const左值引用版本的Func函数。
  • 根本原因就是,右值被引用后会导致右值被存储到特定位置,这时这个右值可以被取到地址,并且可以被修改,所以在PerfectForward函数中调用Func函数时会将t识别成左值,这个在上面的右值前置知识提到过。

也就是说,右值经过一次参数传递后其属性会退化成左值,如果想要在这个过程中保持右值的属性,就需要用到完美转发。

完美转发保持值的属性

要想在参数传递过程中保持其原有的属性,需要在传参时调用forward函数。

template<typename T>
void PerfectForward(T&& t)
{Func(std::forward<T>(t));
}

经过完美转发后,调用PerfectForward函数时传入的是右值就会匹配到右值引用版本的Func函数,传入的是const右值就会匹配到const右值引用版本的Func函数,这就是完美转发的价值,传来什么值就对应转换成什么值类型。

完美转发的使用场景

模拟实现了一个简化版的list类,类当中分别提供了左值引用版本和右值引用版本的push_back和insert函数。

namespace li
{template<class T>struct ListNode{T _data;ListNode* _next = nullptr;ListNode* _prev = nullptr;};template<class T>class list{typedef ListNode<T> node;public://构造函数list(){_head = new node;_head->_next = _head;_head->_prev = _head;}//左值引用版本的push_backvoid push_back(const T& x){insert(_head, x);}//右值引用版本的push_backvoid push_back(T&& x){insert(_head, std::forward<T>(x)); //完美转发}//左值引用版本的insertvoid insert(node* pos, const T& x){node* prev = pos->_prev;node* newnode = new node;newnode->_data = x;prev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}//右值引用版本的insertvoid insert(node* pos, T&& x){node* prev = pos->_prev;node* newnode = new node;newnode->_data = std::forward<T>(x); //完美转发prev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}private:node* _head; //指向链表头结点的指针};
}

定义一个list对象,list容器中存储的就是之前模拟实现的string类,这里分别传入左值和右值调用不同版本的push_back。

int main()
{li:list<li::string> lt;li::string s("1111"); lt.push_back(s);      //调用左值引用版本的push_backlt.push_back("2222"); //调用右值引用版本的push_backreturn 0;
}

调用左值引用版本的push_back函数插入元素时,会调用string原有的operator=函数进行深拷贝,而调用右值引用版本的push_back函数插入元素时,只会调用string的移动赋值进行资源的移动。

  • 因为实现push_back函数时复用了insert函数的代码,对于左值引用版本的push_back函数,在调用insert函数时只能调用左值引用版本的insert函数,而在insert函数中插入元素时会先new一个结点,然后将对应的左值赋值给该结点,因此会调用string原有的operator=函数进行深拷贝。
  • 而对于右值引用版本的push_back函数,在调用insert函数时就可以调用右值引用版本的insert函数,在右值引用版本的insert函数中也会先new一个结点,然后将对应的右值赋值给该结点,因此这里就和调用string的移动赋值函数进行资源的移动。
  • 这个场景中就需要用到完美转发,否则右值引用版本的push_back接收到右值后,该右值的右值属性就退化了,此时在右值引用版本的push_back函数中调用insert函数,也会匹配到左值引用版本的insert函数,最终调用的还是原有的operator=函数进行深拷贝。
  • 此外,除了在右值引用版本的push_back函数中调用insert函数时,需要用完美转发保持右值原有的属性之外,在右值引用版本的insert函数中用右值给新结点赋值时也需要用到完美转发,否则在赋值时也会将其识别为左值,导致最终调用的还是原有的operator=函数。

也就是说,只要想保持右值的属性,在每次右值传参时都需要进行完美转发,实际STL库中也是通过完美转发来保持右值属性的。

注意: 代码中push_back和insert函数的参数T&&是右值引用,而不是万能引用,因为在list对象创建时这个类就被实例化了,后续调用push_back和insert函数时,参数T&&中的T已经是一个确定的类型了,而不是在调用push_back和insert函数时才进行类型推导的。

与STL中的list的区别

将刚才测试代码中的list换成STL当中的list。

  • 调用左值引用版本的push_back插入结点,在构造结点时会调用string的拷贝构造函数。
  • 调用右值引用版本的push_back插入结点,在构造结点时会调用string的移动构造函数。

而用我们模拟实现的list时,调用的却不是string的拷贝构造和移动构造,而对应是string原有的operator=和移动赋值。

原因是因为我们模拟实现的list容器,是通过new操作符为新结点申请内存空间的,在申请内存后会自动调用构造函数对进行其进行初始化,因此在后续用左值或右值对其进行赋值时,就会调用对应的operator=或移动赋值进行深拷贝或资源的转移。
在这里插入图片描述
而STL库中的容器都是通过空间配置器获取内存的,因此在申请到内存后不会调用构造函数对其进行初始化,而是后续用左值或右值对其进行拷贝构造,因此最终调用的就是拷贝构造或移动构造。

如果想要得到与STL相同的实验结果,可以使用malloc函数申请内存,这时就不会自动调用构造函数进行初始化,然后在用定位new的方式用左值或右值对申请到的内存空间进行构造,这时调用的对应就是拷贝构造或移动构造。
在这里插入图片描述

四、总结

在这里插入图片描述

相关文章:

C++11 ——右值引用和移动语义

目录 一、基本概念左值 vs 右值左值引用 vs 右值引用 二、右值引用使用场景和意义左值引用的使用场景左值引用的短板右值引用和移动语义右值引用引用左值右值引用的其他使用场景右值引用总结 三、完美转发右值前置知识万能引用完美转发保持值的属性完美转发的使用场景 四、总结…...

使用交互式半自动化标注工具制作语义分割数据集

参考的初始资源&#xff1a; GitHub项目文档 B站视频 1.安装工具 打开Anaconda Prompt 1.创建虚拟环境 conda create -n isat_env python3.8 conda activate isat_env2.安装GPU版本pytorch 4070 Ti CUDN12.5 pip install torch torchvision torchaudio --index-url https:/…...

阿里二面:聊聊 MySQL 主从同步方案的优缺点

大家好&#xff0c;我是君哥。今天来聊一聊 MySQL 主从架构。 MySQL Replication 是 MySQL 官方提供的主从同步方案&#xff0c;用于将 MySQL 主库的数据同步到从库中&#xff0c;从库可以供应用程序读取数据。 1 简介 Replication 是目前 MySQL 使用最多的灾备方案&#xf…...

YOLO11解决方案之物体模糊探索

概述 Ultralytics提供了一系列的解决方案&#xff0c;利用YOLO11解决现实世界的问题&#xff0c;包括物体计数、模糊处理、热力图、安防系统、速度估计、物体追踪等多个方面的应用。 物体模糊是指对图像或视频中的特定检测对象应用模糊处理&#xff0c;这可以利用YOLO11 模型…...

Elecron 相关介绍以及常见的面试问题

一、深入介绍 Electron Electron 是一个由 GitHub 开发和维护的免费开源软件框架&#xff0c;允许开发者使用 Web 技术&#xff08;HTML、CSS 和 JavaScript&#xff09;构建桌面应用程序 。它将 Chromium 渲染引擎和 Node.js 运行时环境相结合&#xff0c;为开发者提供了一套…...

基于STM32、HAL库的ADAU1701JSTZ音频接口芯片驱动程序设计

一、简介: ADAU1701JSTZ 是 Analog Devices 公司推出的一款高性能、低功耗音频编解码器 (CODEC) 芯片。它专为便携式音频设备设计,集成了麦克风前置放大器、ADC、DAC、耳机放大器等功能模块,支持多种音频接口和采样率,非常适合与 STM32 微控制器配合使用。 主要特性: 24…...

【氮化镓】电子辐照下温度对GaN位移阈能的影响

2024年,华东师范大学的彭胜国等人基于从头算分子动力学(AIMD)方法,研究了低能电子束辐照下温度对氮化镓(GaN)位移阈能(TDE)的影响。实验结果表明,在初始动能40至80 eV的范围内,镓(Ga)和氮(N)原子作为初级击出原子(PKAs)引发的位移对温度呈现不同的敏感性:Ga 的…...

NLTK库: 数据集3-分类与标注语料(Categorized and Tagged Corpora)

NLTK库: 数据集3-分类与标注语料&#xff08;Categorized and Tagged Corpora&#xff09; 1.二分类语料 主要是电影语料&#xff0c;和情绪(积极消极、主观客观)有关&#xff0c;有以下2个语料&#xff1a; 1.1 movie_reviews: IMDb 影评 IMDb&#xff08;Internet Movie …...

物理:人的记忆是由基本粒子构成的吗?

问题: 基因属于人体的一部分,记忆也是人体的一部分,那么为什么基因可以代际遗传,但是记忆却被清空重置。如果基因是由粒子构成,那么记忆是不是也应该由粒子构成?如果记忆是粒子构成的,那么能否说明记忆永恒,即使死亡了身体被分解了,那么只要保证其身体有关的所有粒子被…...

加速度策略思路

一种基于技术指标和动态止损策略的交易方法&#xff0c;旨在提高交易的灵活性和风险控制能力。 1 -动态止损价格计算&#xff1a;该函数通过计算ATR&#xff08;平均真实范围&#xff09;和盈利峰值价&#xff0c;结合加速系数&#xff0c;动态调整止损价格。具体来说&#xf…...

【计算机组成原理】第二部分 存储器--分类、层次结构

文章目录 分类&层次结构0x01 分类按存储介质分类按存取方式分类按在计算机中的作用分类 0x02 层次结构 分类&层次结构 0x01 分类 按存储介质分类 半导体存储器磁表面存储器磁芯存储器光盘存储器 按存取方式分类 存取时间与物理地址无关&#xff08;随机访问&#…...

Spring AI 开发本地deepseek对话快速上手笔记

Spring AI Spring AI是一个旨在推进生成式人工智能应用程序发展的项目&#xff0c;Spring AI的核心目标是提供高度抽象化的组件&#xff0c;作为开发AI应用程序的基础&#xff0c;使得开发者能够以最少的代码改动便捷地交换和优化功能模块‌ 在开发之前先得引入大模型&#xf…...

Python训练打卡Day23

机器学习管道 pipeline 基础概念 pipeline在机器学习领域可以翻译为“管道”&#xff0c;也可以翻译为“流水线”&#xff0c;是机器学习中一个重要的概念。 在机器学习中&#xff0c;通常会按照一定的顺序对数据进行预处理、特征提取、模型训练和模型评估等步骤&#xff0c;以…...

【每天一个知识点】Dip 检验(Dip test)

Dip 检验&#xff08;Dip test&#xff09;是一种用于检验一维数据分布是否为单峰&#xff08;unimodal&#xff09;的非参数统计方法。该检验由 Hartigan 和 Hartigan 于 1985 年提出&#xff0c;通常用于探索性数据分析中&#xff0c;以判断数据是否仅具有一个峰值结构&#…...

AbstractQueuedSynchronizer之AQS

一、前置知识 公平锁和非公平锁&#xff1a; 公平锁&#xff1a;锁被释放以后&#xff0c;先申请的线程先得到锁。性能较差一些&#xff0c;因为公平锁为了保证时间上的绝对顺序&#xff0c;上下文切换更频繁 非公平锁&#xff1a;锁被释放以后&#xff0c;后申…...

【Qt】pro工程文件转CMakeLists文件

1、简述 Qt6以后默认使用cmake来管理工程,之前已经一直习惯使用pro,pro的语法确实很简单、方便。 很多项目都是cmake来管理,将它们加入到Qt项目中,cmake确实是大势所趋。比如,最近将要开发的ROS项目,也是使用的cmake语法。 以前总结的一些Qt代码,已经编写成pro、pri等…...

docker-compose部署thingsboard/tb-cassandra

1、配置 阿里云服务器2H8G 最低 系统:Ubuntu20.0.4 安装 docker 和 docker-compose 环境 ====================安装docker====================== # 更新包 sudo apt update# 安装docker sudo apt install docker.io# 查看是否安装成功 docker --version==================…...

MySQL 日期计算方法 date_sub()、date_add()、datediff() 详解-文中有示例帮助理解

1、date_sub()、date_add() date_sub() 和date_add() 语法相同&#xff0c;只不过一个加一个减。 从日期中减去指定时间间隔 语法&#xff1a; DATE_SUB(start_date, INTERVAL expr unit) start_date: 起始日期&#xff08;如 now() , 字段名&#xff09;。 INTERVAL expr…...

GPT-4.1和GPT-4.1-mini系列模型支持微调功能,助力企业级智能应用深度契合业务需求

微软继不久前发布GPT-4.1系列模型后&#xff0c;Azure OpenAI服务&#xff08;国际版&#xff09;现已正式开放对GPT-4.1和GPT-4.1-mini的微调功能&#xff0c;并通过Azure AI Foundry&#xff08;国际版&#xff09;提供完整的部署和管理解决方案。这一重大升级标志着企业级AI…...

如何将两台虚拟机进行搭桥

虚拟机网络搭桥配置指南 要实现两台虚拟机之间的网络互通&#xff08;"搭桥"&#xff09;&#xff0c;需要根据您的虚拟化平台选择合适的网络模式。以下是主流虚拟化软件的配置方法&#xff1a; 一、VMware 虚拟机互通配置 方案 1&#xff1a;使用桥接模式&#x…...

无缝对接主流电商平台接口,解决货源难题

行业调查显示&#xff0c;大多数代购商每天要花费数小时在淘宝、1688等平台寻找合适商品。手动复制商品链接、整理信息不仅耗时耗力&#xff0c;还容易出错——价格标错、库存不准等问题时有发生&#xff0c;直接影响客户体验。更麻烦的是&#xff0c;不同平台的商品信息格式不…...

GZip+Base64压缩字符串在ios上解压报错问题解决(安卓、PC模拟器正常)

java这边的压缩代码 引入的是java8 jdk自带的gzip压缩&#xff08; java.util.zip.GZIPOutputStream&#xff09;、BASE64Encoder( sun.misc.BASE64Encoder) public static String compress(String str) {if (str ! null && str.length() ! 0) {ByteArrayOutputStream…...

Cookie、 Local Storage、 Session Storage三种客户端存储方式

存储特性对比表 特性CookieLocal StorageSession Storage生命周期可设置过期时间永久保存会话结束自动清除存储容量4KB左右5-10MB5-10MB自动发送到服务器每次HTTP请求头携带不发送不发送访问方式服务端/客户端均可读写仅客户端仅客户端 使用场景及示例 1. Cookie - 用户身份…...

进程等待简单讲解

1. 基本概念 1.1 进程终止与退出状态 当一个进程终止时&#xff0c;它会向其父进程发送一个信号&#xff08;通常是SIGCHLD&#xff09;&#xff0c;并保存退出状态&#xff08;exit status&#xff09;。退出状态可以是一个正常终止的返回值&#xff0c;也可以是一个信号导致…...

基于大模型预测胸椎管狭窄诊疗全流程的研究报告

目录 一、引言 1.1 研究背景与意义 1.2 研究目的与创新点 1.3 研究方法与数据来源 二、胸椎管狭窄症概述 2.1 疾病定义与分类 2.2 病因与发病机制 2.3 流行病学特征 三、大模型技术原理与应用现状 3.1 大模型基本原理 3.2 在医疗领域的应用案例 3.3 用于胸椎管狭窄…...

Oracle OCP认证考试考点详解083系列15

题记&#xff1a; 本系列主要讲解Oracle OCP认证考试考点&#xff08;题目&#xff09;&#xff0c;适用于19C/21C,跟着学OCP考试必过。 71. 第71题&#xff1a; 题目 解析及答案&#xff1a; 关于在 Oracle 18c 及更高版本中基于 Oracle 黄金镜像的安装&#xff0c;以下哪…...

【老飞飞源码】新版高清飞飞源码+数据库+客户端+服务器端完整文件打包

【老飞飞源码】新版高清飞飞源码数据库客户端服务器端完整文件打包下载 编译环境 vs2022 搭建环境 sql2022 测试运行环境 windows 11 本地测试生成搭建都成功 功能包含&#xff1a; pvp排行榜 宠物特效 箱子预览系统 vip系统 宝箱系统 内挂系统 离线摆摊系统 特效帽子系…...

Maven 动态插件配置:Profile的灵活集成实践

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;精通Java编…...

Python爬虫如何应对网站的反爬加密策略?

在当今的互联网环境中&#xff0c;网络爬虫已经成为数据采集的重要工具之一。然而&#xff0c;随着网站安全意识的不断提高&#xff0c;反爬虫技术也越来越复杂&#xff0c;尤其是数据加密策略的广泛应用&#xff0c;给爬虫开发者带来了巨大的挑战。本文将详细介绍Python爬虫如…...

STM32H743输出50%的占空比波形

使用cubeMX进行配置如下&#xff1a; 时钟配置如下&#xff1a; 具体代码如下&#xff1a; /* USER CODE BEGIN Header */ /********************************************************************************* file : main.c* brief : Main program b…...

ios remote debut proxy 怎么开启手机端调试和inspect

手机开启远程调试教程&#xff08;适用于 Chrome / Safari&#xff09; 前端移动端调试指南&#xff5c;适用 iPhone 和 Android&#xff5c;WebDebugX 出品 本教程将详细介绍如何在 iPhone 和 Android 手机上开启网页检查器&#xff0c;配合 WebDebugX 实现远程调试。教程包含…...

GraspVLA:基于Billion-级合成动作数据预训练的抓取基础模型

25年5月来自银河通用&#xff08;Galbot&#xff09;、北大、港大和 BAAI 的论文“GraspVLA: a Grasping Foundation Model Pre-trained on Billion-scale Synthetic Action Data”。 具身基础模型因其零样本泛化能力、可扩展性以及通过少量后训练即可适应新任务的优势&#x…...

BGP联邦实验

一.需求 1.AS1存在两个环回&#xff0c;一个地址为192.168.1.0/24&#xff0c;该地址不能再任何协议中宣告 AS3存在两个环回&#xff0c;一个地址为192.168.2.0/24&#xff0c;该地址不能再任何协议中宣告 AS1还有一个环回地址为10.1.1.0/24&#xff0c;AS3另一个环回地址是…...

自动化测试基础知识详解

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 自动化测试是指利用自动化工具和脚本&#xff0c;模拟人工操作进行软件测试的过程。它在软件开发中扮演着非常重要的角色&#xff0c;可以提高测试效率、降低成本…...

Java后端快速生成验证码

Hutool是一个小而全的Java工具类库&#xff0c;它提供了很多实用的工具类&#xff0c;包括但不限于日期处理、加密解密、文件操作、反射操作、HTTP客户端等。 核心工具类&#xff1a;CaptchaUtil&#xff0c;CaptchaUtil 是 Hutool 提供的一个工具类&#xff0c;用于创建各种类…...

【愚公系列】《Manus极简入门》036-物联网系统架构师:“万物互联师”

&#x1f31f;【技术大咖愚公搬代码&#xff1a;全栈专家的成长之路&#xff0c;你关注的宝藏博主在这里&#xff01;】&#x1f31f; &#x1f4e3;开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主&#xff01; &#x1f…...

主流高防服务器技术对比与AI防御方案实战

1. 高防服务器核心能力对比 当前市场主流高防服务商&#xff08;如阿里云、腾讯云、华为云&#xff09;的核心防御能力集中在流量清洗与静态规则防护&#xff0c;但面临以下挑战&#xff1a; 静态防御瓶颈&#xff1a;传统方案依赖预定义规则&#xff0c;对新型攻击&#xff…...

带格式的可配置文案展示

方案一&#xff08;格式包含颜色换行等&#xff09; 服务端&#xff1a;配置后接口输出带标签的字符串&#xff0c;但是尖括号不能被转义前端&#xff1a;v-html接受字符串&#xff08;vue项目&#xff09;&#xff0c;原生用innerHTML赋值 方案二&#xff08;格式针对只存在…...

湖南大学3D场景问答最新综述!3D-SQA:3D场景问答助力具身智能场景理解

作者&#xff1a; Zechuan Li, Hongshan Yu, Yihao Ding, Yan Li, Yong He, Naveed Akhtar 单位&#xff1a;湖南大学&#xff0c;墨尔本大学&#xff0c;悉尼大学&#xff0c;安徽大学 论文标题&#xff1a;Embodied Intelligence for 3D Understanding: A Survey on 3D Sce…...

【PyTorch】深度学习实践——第二章:线性模型

参考&#xff1a;刘二老师的《PyTorch深度学习实践》完结合集 本章实现了一个简单的线性回归模型&#xff0c;用于学习输入x和输出y之间的线性关系(yw*x)。 一、代码细节 1.数据准备 x_data [1.0, 2.0, 3.0] y_data [2.0, 4.0, 6.0]定义了训练数据&#xff0c;x和y之间显然…...

【Python 中文编码】

在 Python 中处理中文编码问题时&#xff0c;需重点关注文件编码声明、字符串编码转换及环境配置。以下是分步指南和最佳实践&#xff1a; 一、Python 3 的默认编码行为 Python 3.x&#xff1a;默认使用 UTF-8 编码&#xff08;与 Python 2.x 的 ASCII 默认编码不同&#xff0…...

Excel宏和VBA

Excel宏和VBA&#xff08;Visual Basic for Applications&#xff09;是自动化Excel操作的强大工具&#xff0c;可帮助用户批量处理数据、自定义功能、提升效率。以下是详细使用方法及示例&#xff1a; --- ### **一、基础操作** #### 1. **录制宏** - **步骤**&#xff1…...

1688 API 接口使用限制

在使用 1688 API 接口时&#xff0c;需要注意以下几方面的限制和注意事项&#xff0c;以确保合规使用并避免不必要的问题。 一、调用频率限制 1688 平台对 API 接口的调用频率通常有限制&#xff0c;以防止滥用和对服务器造成过大压力。具体限制如下&#xff1a; 免费版&…...

5. 动画/过渡模块 - 交互式仪表盘

5. 动画/过渡模块 - 交互式仪表盘 案例&#xff1a;数据分析仪表盘 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title></head><style type"text/css">.dashboard {font-family: Arial…...

数据擦除标准:1-Pass vs. 3-Pass vs. 7-Pass有什么区别,哪个更好?

虽然像美国国防部(DoD)5220.22-M这样的旧标准提倡多次覆盖,但像NIST 800-88和新兴的IEEE 2883标准这样的新指南已经改变了对数据擦除效果的看法。在这篇博客中,我们解释了不同的擦除方法,并分析了旧标准在新时代是否仍然相关。 理解数据擦除方法 数据擦除包括用0、1或随…...

MySQL推荐书单:从入门到精通

给大家介绍一些 MySQL 从入门到精通的经典书单&#xff0c;可以基于不同学习阶段的需求进行选择。 入门 MySQL必知必会 这本书继承了《SQL必知必会》的优点&#xff0c;专门针对 MySQL 用户&#xff0c;没有过多阐述数据库基础理论&#xff0c;而是紧贴实战&#xff0c;直接从…...

Rodrigues旋转公式-绕任意轴旋转

Rodrigues旋转公式 给定旋转轴单位向量 k ( k x , k y , k z ) \mathbf{k}(k_x,k_y,k_z) k(kx​,ky​,kz​)和旋转角度 θ \theta θ&#xff0c;旋转矩阵 R R R可以表示为&#xff1a; R I sin ⁡ θ K ( 1 − cos ⁡ θ ) K 2 RI\sin \theta K(1-\cos \theta)K^2 RIsin…...

【大模型面试每日一题】Day 17:解释MoE(Mixture of Experts)架构如何实现模型稀疏性,并分析其训练难点

【大模型面试每日一题】Day 17&#xff1a;解释MoE&#xff08;Mixture of Experts&#xff09;架构如何实现模型稀疏性&#xff0c;并分析其训练难点 &#x1f4cc; 题目重现 &#x1f31f;&#x1f31f; 面试官:解释MoE&#xff08;Mixture of Experts&#xff09;架构如何…...

Datawhale 5月coze-ai-assistant 笔记1

课程地址&#xff1a; coze-ai-assistant-课程摘要 | Datawhalehttps://www.datawhale.cn/learn/summary/105 动手实践 链接&#xff1a;https://www.coze.cn/home 作业&#xff1a;智能体链接地址扣子扣子是新一代 AI 大模型智能体开发平台。整合了插件、长短期记忆、工作…...

2025.5.13总结

想要成为自己想要成为的那个人&#xff0c;并不是一件容易的事情。在我报口才课的时候&#xff0c;老师一针见血的指出了我的不足。因为不敢&#xff0c;所以不做&#xff0c;因为不去做&#xff0c;所以不会&#xff0c;而正因为不会&#xff0c;也导致了你不敢。当我听到这个…...