news 2026/5/28 4:34:01

c++11(简介与右值引用)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
c++11(简介与右值引用)

1. C++11简介

在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了 C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞 进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。

相比于 C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中 约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。

相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更 强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以很重要。C++11增加的语法特性非常篇幅非常多,主要讲解实际中比较实用的语法。C++11 - cppreference.com

2. 统一的列表初始化

2.1 {}初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:

struct Point { int _x; int _y; }; int main() { int array1[] = { 1, 2, 3, 4, 5 }; int array2[5] = { 0 }; Point p = { 1, 2 }; return 0; }

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自 定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

struct Point { int _x; int _y; }; int main() { int x1 = 1; int x2{ 2 }; int array1[]{ 1, 2, 3, 4, 5 }; int array2[5]{ 0 }; Point p{ 1, 2 }; // C++11中列表初始化也可以适用于new表达式中 int* pa = new int[4]{ 0 }; return 0; }

创建对象时也可以使用列表初始化方式调用构造函数初始化

class Date { public: Date(int year, int month, int day) :_year(year) ,_month(month) ,_day(day) { cout << "Date(int year, int month, int day)" << endl; } private: int _year; int _month int _day; }; int main() { Date d1(2022, 1, 1); // old style // C++11支持的列表初始化,这里会调用构造函数初始化 Date d2{ 2022, 1, 2 }; Date d3 = { 2022, 1, 3 }; return 0; }

2.2 std::initializer_list

std::initializer_list的介绍文档: cplusplus.com/reference/initializer_list/initializer_list/

C++11中新增了initializer_list容器,该容器没有提供过多的成员函数。

  • 提供了begin和end函数,用于支持迭代器遍历。
  • 以及size函数支持获取容器中的元素个数。

initializer_list本质就是一个大括号括起来的列表,如果用auto关键字定义一个变量来接收一个大括号括起来的列表,然后以typeid(变量名).name()的方式查看该变量的类型,此时会发现该变量的类型就是initializer_list。

int main() { // the type of il is an initializer_list auto il = { 10, 20, 30 }; cout << typeid(il).name() << endl; return 0; }

initializer_list的使用场景

nitializer_list容器没有提供对应的增删查改等接口,因为initializer_list并不是专门用于存储数据的,而是为了让其他容器支持列表初始化的。比如:

class Date { public: Date(int year, int month, int day) :_year(year) , _month(month) , _day(day) { cout << "Date(int year, int month, int day)" << endl; } private: int _year; int _month; int _day; }; int main() { //用大括号括起来的列表对容器进行初始化 vector<int> v = { 1, 2, 3, 4, 5 }; list<int> l = { 10, 20, 30, 40, 50 }; vector<Date> vd = { Date(2022, 8, 29), Date{ 2022, 8, 30 }, { 2022, 8, 31 } }; map<string, string> m{ make_pair("sort", "排序"), { "insert", "插入" } }; //用大括号括起来的列表对容器赋值 v = { 5, 4, 3, 2, 1 }; return 0; }

C++98并不支持直接用列表对容器进行初始化,这种初始化方式是在C++11引入initializer_list后才支持的。

而这些容器之所以支持使用列表进行初始化,根本原因是因为C++11给这些容器都增加了一个构造函数,这个构造函数就是以initializer_list作为参数的。

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加 std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator= 的参数,这样就可以用大括号赋值。

当用列表对容器进行初始化时,这个列表被识别成initializer_list类型,于是就会调用这个新增的构造函数对该容器进行初始化。

这个新增的构造函数要做的就是遍历initializer_list中的元素,然后将这些元素依次插入到要初始化的容器当中即可。

让模拟实现的vector也支持{}初始化和赋值

namespace bit { template<class T> class vector { public: typedef T* iterator; vector(initializer_list<T> l) { _start = new T[l.size()]; _finish = _start + l.size(); _endofstorage = _start + l.size(); iterator vit = _start; typename initializer_list<T>::iterator lit = l.begin(); while (lit != l.end()) { *vit++ = *lit++; } //for (auto e : l) // *vit++ = e; } vector<T>& operator=(initializer_list<T> l) { vector<T> tmp(l); std::swap(_start, tmp._start); std::swap(_finish, tmp._finish); std::swap(_endofstorage, tmp._endofstorage); return *this; } private: iterator _start; iterator _finish; iterator _endofstorage; }; }

3. 声明

c++11提供了多种简化声明的方式,尤其是在使用模板时。

3.1 auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局 部的变量默认就是自动存储类型,所以auto就没什么价值了。

C++11中废弃auto原来的用法,将 其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初 始化值的类型。

int main() { int i = 10; auto p = &i; auto pf = strcpy; cout << typeid(p).name() << endl; cout << typeid(pf).name() << endl; map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} }; //map<string, string>::iterator it = dict.begin(); auto it = dict.begin(); return 0; }

3.2 decltype

关键字decltype将变量的类型声明为表达式指定的类型。

// decltype的一些使用使用场景 template<class T1, class T2> void F(T1 t1, T2 t2) { decltype(t1 * t2) ret; cout << typeid(ret).name() << endl; } int main() { const int x = 1; double y = 2.2; decltype(x * y) ret; // ret的类型是double decltype(&x) p; // p的类型是int* cout << typeid(ret).name() << endl; cout << typeid(p).name() << endl; F(1, 'a'); return 0; }

3.3 nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示 整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif

4 范围for循环

C++98中我们要遍历一个数组,可以按照以下方式:

int main() { int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //将数组元素值全部乘以2 for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) { arr[i] *= 2; } //打印数组中的所有元素 for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) { cout << arr[i] << " "; } cout << endl; return 0; }

对于一个有范围的集合而言,循环是多余的,有时还容易犯错。所以C++11中引入了基于范围的for循环,for循环后的括号由冒号分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。比如

int main() { int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //将数组元素值全部乘以2 for (auto& e : arr) { e *= 2; } //打印数组中的所有元素 for (auto e : arr) { cout << e << " "; } cout << endl; return 0; }

仍然可用continue来结束本次循环,也可以用break来跳出整个循环。

范围for的使用条件

一、for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

二、迭代的对象要支持++和==操作
范围for本质上是由迭代器支持的,在代码编译的时候,编译器会自动将范围for替换为迭代器的形式。而由于在使用迭代器遍历时需要对对象进行++和==操作,因此使用范围for的对象也需要支持++和==操作。

5、STL中一些变化

新容器

C++11中新增了四个容器,分别是array、forward_list、unordered_map和unordered_set。用橘色圈起来是C++11中的几个新容器,但是实际最有用的是unordered_mapunordered_set

array容器


array容器本质就是一个静态数组,即固定大小的数组。

array容器有两个模板参数,第一个模板参数代表的是存储的类型,第二个模板参数是一个非类型模板参数,代表的是数组中可存储元素的个数。比如:

int main() { array<int, 10> a1; //定义一个可存储10个int类型元素的array容器 array<double, 5> a2; //定义一个可存储5个double类型元素的array容器 return 0; }

array容器与普通数组对比:

array容器与普通数组一样,支持通过[]访问指定下标的元素,也支持使用范围for遍历数组元素,并且创建后数组的大小也不可改变。
array容器与普通数组不同之处就是,array容器用一个类对数组进行了封装,并且在访问array容器中的元素时会进行越界检查。用[]访问元素时采用断言检查,调用at成员函数访问元素时采用抛异常检查。
而对于普通数组来说,一般只有对数组进行写操作时才会检查越界,如果只是越界进行读操作可能并不会报错。
但array容器与其他容器不同的是,array容器的对象是创建在栈上的,因此array容器不适合定义太大的数组。

forward_list容器


forward_list容器本质就是一个单链表

forward_list很少使用,原因如下:

forward_list只支持头插头删,不支持尾插尾删,因为单链表在进行尾插尾删时需要先找尾,时间复杂度为O(N)。
forward_list提供的插入函数叫做insert_after,也就是在指定元素的后面插入一个元素,而不像其他容器是在指定元素的前面插入一个元素,因为单链表如果要在指定元素的前面插入元素,还要遍历链表找到该元素的前一个元素,时间复杂度为O(N)。
forward_list提供的删除函数叫做erase_after,也就是删除指定元素后面的一个元素,因为单链表如果要删除指定元素,还需要还要遍历链表找到指定元素的前一个元素,时间复杂度为O(N)。
因此一般情况下要用链表我们还是选择使用list容器。

unordered_map和unordered_set容器

后面整合到stl里讲
unordered_map和unordered_set容器底层采用的都是哈希表。

6、字符串转换函数

C++11提供了各种内置类型与string之间相互转换的函数,比如to_string、stoi、stol、stod等函数。

内置类型转换为string
将内置类型转换成string类型统一调用to_string函数,因为to_string函数为各种内置类型重载了对应的处理函数。

string转换成内置类型
如果要将string类型转换成内置类型,则调用对应的转换函数即可

容器中的一些新方法

  • 提供了一个以initializer_list作为参数的构造函数,用于支持列表初始化。
  • 提供了cbegin和cend方法,用于返回const迭代器。
  • 提供了emplace系列方法,并在容器原有插入方法的基础上重载了一个右值引用版本的插入函数,用于提高向容器中插入元素的效率。

7 右值引用和移动语义

7.1 左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们 之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名

什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋 值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左 值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

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); // 这里编译会报错:error C2106: “=”: 左操作数必须为左值 10 = 1; x + y = 1; fmin(x, y) = 1; return 0; }

注意右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可 以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地 址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇, 这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

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

7.2 左值引用与右值引用比较

左值引用总结:

1. 左值引用只能引用左值,不能引用右值。

2. 但是const左值引用既可引用左值,也可引用右值。

int main() { // 左值引用只能引用左值,不能引用右值。 int a = 10; int& ra1 = a; // ra为a的别名 //int& ra2 = 10; // 编译失败,因为10是右值 // const左值引用既可引用左值,也可引用右值。 const int& ra3 = 10; const int& ra4 = a; return 0; }

右值引用总结:

1. 右值引用只能右值,不能引用左值。

2. 但是右值引用可以move以后的左值。

int main() { // 右值引用只能右值,不能引用左值。 int&& r1 = 10; // error C2440: “初始化”: 无法从“int”转换为“int &&” // message : 无法将左值绑定到右值引用 int a = 10; int&& r2 = a; // 右值引用可以引用move以后的左值 int&& r3 = std::move(a); return 0; }

右值引用使用场景和意义

左值引用(&)既能引用左值,const左值引用(const &)还能引用右值,为什么C++11还要引入右值引用(&&)?
答案:为了区分左值与右值,从而在合适的时机采用移动语义替代深拷贝,大幅提高性能。
下面通过一个自定义bit::string类的演变,来看左值引用的短板和右值引用如何补齐。


一、左值引用的短板(以未加入移动语义的string为例)

假设我们只实现了拷贝构造拷贝赋值(参数为const string&):

// 拷贝构造(深拷贝) string(const string& s) :_str(nullptr) { cout << "string(const string& s) -- 深拷贝" << endl; string tmp(s._str); // 临时对象,调用构造函数分配新内存并拷贝数据 swap(tmp); // 与本对象交换,tmp析构时释放原空_str } // 拷贝赋值(深拷贝) string& operator=(const string& s) { cout << "string& operator=(const string& s) -- 深拷贝" << endl; string tmp(s); // 用s深拷贝出一个tmp swap(tmp); // 交换资源 return *this; }

场景问题:

bit::string GetString() { bit::string str("hello"); return str; // 返回局部对象,str即将销毁 } bit::string s = GetString();
  • 如果不开启优化,GetString()返回的是一个右值(将亡值),它马上就会析构。

  • 编译器只能用const string&去匹配,只能调用拷贝构造,对临时对象再进行一次深拷贝——浪费

  • 即:明知道这个返回的临时对象的资源可以直接“偷”过来用,却不得不拷贝一份再销毁原对象,这就是左值引用无法区分右值造成的性能短板


二、右值引用与移动语义:补齐短板

右值引用string&&只能绑定到右值,它的作用就是标记出“即将销毁的对象”,让编译器选择移动构造/移动赋值,转移资源而非拷贝。

1. 移动构造函数(新增)
// 移动构造 —— 参数是右值引用 string(string&& s) :_str(nullptr) ,_size(0) ,_capacity(0) { cout << "string(string&& s) -- 移动语义" << endl; swap(s); // 直接交换资源指针 } // 调用场景:用一个右值初始化新对象 // s被交换后置空,析构时安全地delete[] nullptr

要点:

  • 不分配新内存,不拷贝数据。

  • 通过swap临时对象的资源直接转移给当前对象。

  • 临时对象被掏空后变成安全状态,析构不会有问题。

2. 移动赋值运算符(新增)
// 移动赋值 —— 参数是右值引用 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动语义" << endl; swap(s); // 直接交换资源 return *this; } // 调用场景:用一个右值给已有对象赋值 // 被赋值对象原有的资源由s带走,赋值后s被析构时释放该资源

为什么移动赋值不需要先清空自己?

  • 直接swap(s):原对象的资源指针交给了ss在函数结束时销毁,顺便帮我们释放了旧资源。

  • 这等同于“用右值的资源替换自己,同时丢弃旧资源”。


三、性能对比:何时调用移动语义

假设类同时提供了拷贝和移动版本:

表达式实参类别调用的函数
string s2(s1);s1是左值拷贝构造const string&
string s3(std::move(s1));右值(xvalue)移动构造string&&
string s4(GetString());右值(纯右值)移动构造
s2 = s1;左值拷贝赋值
s2 = GetString();右值移动赋值
s2 = std::move(s3);右值移动赋值

没有移动语义时:以上所有涉及右值的操作都会退化成深拷贝。
有移动语义后:当源对象是右值,编译器会优先匹配移动版本,只交换指针/大小等,时间复杂度 O(1),没有内存分配和数据拷贝。


四、bit::string 类的完整实现拆解
namespace bit { 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]; strcpy(_str, str); } // 交换函数(工具) void swap(string& s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } // ------- 拷贝语义(针对左值) ------- // 拷贝构造(深拷贝) string(const string& s) : _str(nullptr) { // 利用构造函数创建临时对象,再交换 string tmp(s._str); swap(tmp); } // 拷贝赋值(深拷贝) string& operator=(const string& s) { string tmp(s); swap(tmp); return *this; } // ------- 移动语义(针对右值) ------- // 移动构造 string(string&& s) : _str(nullptr), _size(0), _capacity(0) { swap(s); // 把右值的资源偷过来 } // 移动赋值 string& operator=(string&& s) { swap(s); // 右值接管旧资源并自动销毁 return *this; } ~string() { delete[] _str; _str = nullptr; } // ... reserve, push_back, operator+= 等略 private: char* _str; size_t _size; size_t _capacity; // 不含'\0' }; }

五、使用场景与意义总结
  1. 返回值优化之外的兜底
    即使编译器没有做(或无法做)返回值优化(RVO),移动构造也能保证返回局部对象时只转移资源。

  2. 容器操作性能飞跃
    vector<string>push_back(临时string),或者vector自身扩容重新分配内存时,如果元素类型支持移动语义,则只转移资源,效率远高于拷贝。

  3. 不需要修改拷贝接口,与旧代码兼容
    拷贝重载依然保留给左值,移动重载自动为右值服务,代码整洁且安全。

  4. 明确表达“资源所有权转移”
    使用std::move可以把左值转为右值,表示“我不再需要这个对象的内容,你可以拿走”,这在工厂模式、对象池等场景非常实用。


核心结论:
右值引用的真正意义不是“可以引用右值”(const左值引用也能),而是通过类型区分出“将亡对象”,从而触发移动语义,将深拷贝变成廉价的资源交换,彻底消除左值引用在这类场景下的性能瓶颈。

左值引用的使用场景:

做参数和做返回值都可以提高效率。

void func1(bit::string s) {} void func2(const bit::string& s) {} int main() { bit::string s1("hello world"); // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值 func1(s1); func2(s1); // string operator+=(char ch) 传值返回存在深拷贝 // string& operator+=(char ch) 传左值引用没有拷贝提高了效率 s1 += '!'; return 0; }

左值引用的短板:

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回, 传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

namespace bit { bit::string to_string(int value) { bool flag = true; if (value < 0) { flag = false; value = 0 - value; } bit::string str; while (value > 0) { int x = value % 10; value /= 10; str += ('0' + x); } if (flag == false) { str += '-'; } std::reverse(str.begin(), str.end()); return str; } } int main() { // 在bit::string to_string(int value)函数中可以看到,这里 // 只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷 贝构造)。 bit::string ret1 = bit::to_string(1234); bit::string ret2 = bit::to_string(-1234); return 0; }

右值引用和移动语义解决上述问题:

在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不 用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

// 移动构造 string(string&& s) :_str(nullptr) ,_size(0) ,_capacity(0) { cout << "string(string&& s) -- 移动语义" << endl; swap(s); } int main() { bit::string ret2 = bit::to_string(-1234); return 0; }

再运行上面bit::to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用 了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。

不仅仅有移动构造,还有移动赋值:

在bit::string类中增加移动赋值函数,再去调用bit::to_string(1234),不过这次是将 bit::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。

// 移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动语义" << endl; swap(s); return *this; } int main() { bit::string ret1; ret1 = bit::to_string(1234); return 0; } // 运行结果: // string(string&& s) -- 移动语义 // string& operator=(string&& s) -- 移动语义

这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象 接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象,但是 我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时 对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。

STL中的容器都是增加了移动构造和移动赋值:

cplusplus.com/reference/string/string/string/

cplusplus.com/reference/vector/vector/vector/

7.4 右值引用引用左值及其一些更深入的使用场景分析

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能 真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move 函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性, 它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

template<class _Ty> inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT { // forward _Arg as movable return ((typename remove_reference<_Ty>::type&&)_Arg); } int main() { bit::string s1("hello world"); // 这里s1是左值,调用的是拷贝构造 bit::string s2(s1); // 这里我们把s1 move处理以后, 会被当成右值,调用移动构造 // 但是这里要注意,一般是不要这样用的,因为我们会发现s1的 // 资源被转移给了s3,s1被置空了。 bit::string s3(std::move(s1)); return 0; }

STL容器插入接口函数也增加了右值引用版本:

cplusplus.com/reference/list/list/push_back/

cplusplus.com/reference/vector/vector/push_back/

void push_back (value_type&& val); int main() { list<bit::string> lt; bit::string s1("1111"); // 这里调用的是拷贝构造 lt.push_back(s1); // 下面调用都是移动构造 lt.push_back("2222"); lt.push_back(std::move(s1)); return 0; } 运行结果: // string(const string& s) -- 深拷贝 // string(string&& s) -- 移动语义 // string(string&& s) -- 移动语义

7.5 完美转发

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

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

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

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

但实际调用PerfectForward函数时传入左值和右值,最终都匹配到了左值引用版本的Func函数,调用PerfectForward函数时传入const左值和const右值,最终都匹配到了const左值引用版本的Func函数。
根本原因就是,右值被引用后会导致右值被存储到特定位置,这时这个右值可以被取到地址,并且可以被修改,所以在PerfectForward函数中调用Func函数时会将t识别成左值。
也就是说,右值经过一次参数传递后其属性会退化成左值,如果想要在这个过程中保持右值的属性,就需要用到完美转发。

void Fun(int &x){ cout << "左值引用" << endl; } void Fun(const int &x){ cout << "const 左值引用" << endl; } void Fun(int &&x){ cout << "右值引用" << endl; } void Fun(const int &&x){ cout << "const 右值引用" << endl; } // 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。 // 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力, // 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值, // 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发 template<typename T> void PerfectForward(T&& t) { Fun(t); } int main() { PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b = 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }

std::forward 完美转发在传参的过程中保留对象原生类型属性

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

void Fun(int &x){ cout << "左值引用" << endl; } void Fun(const int &x){ cout << "const 左值引用" << endl; } void Fun(int &&x){ cout << "右值引用" << endl; } void Fun(const int &&x){ cout << "const 右值引用" << endl; } // std::forward<T>(t)在传参的过程中保持了t的原生类型属性。 template<typename T> void PerfectForward(T&& t) { Fun(std::forward<T>(t)); } int main() { PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b = 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }

完美转发实际中的使用场景:

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

代码如下:

namespace cl { 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_back void push_back(const T& x) { insert(_head, x); } //右值引用版本的push_back void push_back(T&& x) { insert(_head, std::forward<T>(x)); //完美转发 } //左值引用版本的insert void 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; } //右值引用版本的insert void 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() { cl::list<cl::string> lt; cl::string s("1111"); lt.push_back(s); //调用左值引用版本的push_back lt.push_back("2222"); //调用右值引用版本的push_back return 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的方式用左值或右值对申请到的内存空间进行构造,这时调用的对应就是拷贝构造或移动构造。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/28 4:29:46

因果推断模型升级:超越ROI的多维决策框架与工程实践

1. 项目概述&#xff1a;当模型升级不再是简单的价格标签在数据科学和机器学习驱动的决策领域&#xff0c;因果推断正从一个学术概念迅速转变为商业应用的核心引擎。无论是评估营销活动的真实效果、优化产品功能&#xff0c;还是衡量政策干预的长期影响&#xff0c;一个稳健的因…

作者头像 李华
网站建设 2026/5/28 4:25:55

AI赋能硬件开发:从自然语言描述自动生成Arduino电路图与代码

1. 项目概述&#xff1a;当AI开始“画”电路图作为一名玩了十多年Arduino的硬件爱好者&#xff0c;我最近干了一件挺有意思的事&#xff1a;我开发了一个AI工具&#xff0c;它能根据你的项目描述&#xff0c;自动生成对应的电路接线图和Arduino代码。简单来说&#xff0c;就是你…

作者头像 李华
网站建设 2026/5/28 4:24:36

ResNet-50错误诊断与调试指南:常见问题与解决方案大全

ResNet-50错误诊断与调试指南&#xff1a;常见问题与解决方案大全 【免费下载链接】resnet-50 项目地址: https://ai.gitcode.com/hf_mirrors/microsoft/resnet-50 ResNet-50是深度学习领域中广泛应用的图像分类模型&#xff0c;但其在实际部署和运行过程中可能会遇到各…

作者头像 李华
网站建设 2026/5/28 4:24:29

FFmpeg错误码背后的设计巧思:从AVERROR_BUG的ASCII码到高效错误处理

FFmpeg错误码背后的设计巧思&#xff1a;从AVERROR_BUG的ASCII码到高效错误处理在多媒体处理领域&#xff0c;FFmpeg堪称瑞士军刀般的存在。但鲜为人知的是&#xff0c;这个开源项目在错误处理机制上的设计同样精妙绝伦。当开发者第一次看到AVERROR_BUG这个宏定义时&#xff0c…

作者头像 李华
网站建设 2026/5/28 4:18:58

如何彻底解决微信聊天记录丢失问题:WeChatMsg完整备份方案

如何彻底解决微信聊天记录丢失问题&#xff1a;WeChatMsg完整备份方案 【免费下载链接】WeChatMsg 提取微信聊天记录&#xff0c;将其导出成HTML、Word、CSV文档永久保存&#xff0c;对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/W…

作者头像 李华
网站建设 2026/5/28 4:18:19

终极指南:如何用ok-ww解放双手,轻松自动化鸣潮日常任务

终极指南&#xff1a;如何用ok-ww解放双手&#xff0c;轻松自动化鸣潮日常任务 【免费下载链接】ok-wuthering-waves 鸣潮 后台自动战斗 自动刷声骸 一键日常 Automation for Wuthering Waves 项目地址: https://gitcode.com/GitHub_Trending/ok/ok-wuthering-waves 每天…

作者头像 李华