【C++11】列表初始化、右值引用和移动语义、引用折叠、完美转发
C++11
- 一.C++的发展历史
- 二.列表初始化
- 1.C++98的{}
- 2.C++11的{}
- 3.C++11中的std::initializer_list
- 三.右值引用和移动语义
- 1.左值和右值
- 2.左值引用和右值引用
- 3.引用延长生命周期
- 4.左值和右值的参数匹配
- 5.右值引用和移动语义使用场景
- 1.左值引用使用场景
- 2.移动构造和移动赋值
- 3.右值引用和移动语义解决传值返回问题
- 1.右值对象构造,只有拷贝构造,没有移动构造的场景
- 2.右值对象构造,既有拷贝构造,也有移动构造的场景
- 3.右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
- 4.右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
- 6.类型分类
- 7.引用折叠
- 8.完美转发
一.C++的发展历史
- 1998年,C++标准委员会成立。C++98是C++的第一个国际标准,标志着C++语言的成熟和稳定,并计划每五年对C++标准进行一次更新。
- C++03是对C++98的微小修订,没有引入重大新特性。
- C++11是C++的第⼆个重要版本,并且是从C++98起的最重要的更新。C++11的发布对C++语言的发展产生了深远的影响,它使得C++语言更加现代化和高效,提高了开发者的编程体验和代码质量。
- 在C++11由ISO在2011年8月12日采纳前,人们曾使用名称"C++0x",因为它曾被期待在2010年之前发布(由于C++标准委员会长期摆烂,导致五年计划延期)。经过多年的延期和讨论,C++标准委员会终于在2011年完成了新标准的制定,并将新标准命名为C++11。
- C++03与C++11期间花了8年时间,故而这是迄今为止最长的版本间隔。从那时起,C++有规律地每3年更新一次。
- 非官方文档
- 官方中文文档
- 官方英文文档
说明:第一个链接不是C++官方文档,标准也只更新到C++11,但是以头文件形式呈现,内容比较易看好懂。后两个链接分别是C++官方文档的中文版和英文版,信息很全,更新到了最新的C++标准,但是相比第一个不那么易看;几个文档各有优势,可以结合着使用。
二.列表初始化
1.C++98的{}
C++98中一般数组和结构体可以用{}进行初始化。
struct Point
{int _x;int _y;
};int main()
{int arr1[] = { 1, 2, 3, 4, 5 };int arr2[5] = { 0 };Point p = { 1, 2 };return 0;
}
2.C++11的{}
- C++11以后想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化。
- 内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。
- {}初始化的过程中,可以省略掉=。
- C++11列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很方便。
struct Point
{int _x;int _y;
};class Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){cout << "Date(const Date& d)" << endl;}private:int _year;int _month;int _day;
};int main()
{//C++98:数组和结构体支持列表初始化int a1[] = { 1, 2, 3, 4, 5 };int a2[5] = { 0 };Point p = { 1, 2 };//C++11://内置类型支持列表初始化int x = { 2 }; //低层就是int x = 2; 无区别//自定义类型也支持列表初始化//本质是用{2025, 1, 4}构造一个Date临时对象//临时对象再去拷贝构造1,编译器优化为合二为一变成{2025, 1, 4}直接构造初始化d1//运行可以验证上面的理论,发现没有调用拷贝构造Date d1 = { 2025, 1, 4 }; //C++11:多参数构造函数支持隐式类型转换//这里d2引用的是{2025, 1, 5}构造的临时对象,临时对象具有常性,需要加上const//匿名对象是手动创建的,临时对象是编译器生成的,二者都具有常性,引用时都需要加上constconst Date& d2 = { 2025, 1, 5 };//C++98支持单参数的隐式类型转换,也可以不用{}Date d3 = { 2025 };Date d4 = 2025;string s = "1111";//列表初始化:可以省略掉=Point p1{ 1, 2 };int x2{ 2 };Date d6{ 2025, 1, 6 };const Date& d7{ 2025, 1, 6 };//不支持以下,只有列表初始化,才能省略= //Date d8 2025;vector<Date> v;v.push_back(d1);v.push_back(Date(2025, 1, 1));//比起有名对象和匿名对象传参,这里{}更有性价比v.push_back({ 2025, 1, 1 });map<string, int> dict;dict.insert({ "123",123 });return 0;
}
3.C++11中的std::initializer_list
- 上面的初始化已经方便,但是对象容器初始化还是不太方便,比如一个vector对象,我想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持,vector< int > v1 = {1, 2, 3}; vector< int > v2 = {1, 2, 3, 4, 5};
- C++11库中提出了一个std::initializer_list的类, auto il = { 10, 20, 30 },这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
- 文档:initializer_list。std::initializer_list支持迭代器遍历。
- 容器支持一个std::initializer_list的构造函数,也就支持任意多个值构成的 {x1,x2,x3…} 进行初始化。STL中的容器支持任意多个值构成的 {x1,x2,x3…} 进行初始化,就是通过std::initializer_list的构造函数支持的。
int main()
{initializer_list<int> il = { 1,2,3,4,5,6,7,8,9 };auto it = il.begin();while (it != il.end()){cout << *it << " ";++it;}cout << endl;cout << il.size() << endl;//构造+拷贝构造:优化为直接构造vector<int> v1 = { 1,2,3,4,5,6,7,8,9 };const vector<int>& v2 = { 1,2,3,4,5,6,7,8,9 };const vector<int>& v3{ 1,2,3,4,5,6,7,8,9 };//构造:单参数(initializer_list参数)支持隐式类型转换vector<int> v4({ 1,2,3,4,5,6,7,8,9 });//内部多参数隐式类型转换构造pair临时对象,外部单参数隐式类型转换构造initializer_list临时对象,最后拷贝构造:优化为直接构造map<string, string> dict = { {"aaa","bbb"},{"ccc","ddd"},{"eee","fff"} };return 0;
}
三.右值引用和移动语义
C++98中的引用就叫做左值引用,而C++11提供了右值引用。无论左值引用还是右值引用,都是给对象取别名。
1.左值和右值
- 左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,
左值可以取地址
,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。 - 右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边(不能修改),
右值不能取地址
。 - 值得一提的是,左值的英文简写为lvalue,右值的英文简写为rvalue。传统认为它们分别是left value、right value的缩写。现代C++中,lvalue被解释为locator value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,而rvalue被解释为read value,指的是那些可以提供数据值,但是不可以寻址,例如:
临时变量
,字面量常量
,匿名对象
,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址
。
int main()
{//左值:可以取地址 //以下的 p、b、c、*p、s、s[0] 就是常见的左值int* p = new int(10);int b = 10;const int c = b;*p = 20;string s("1111");s[0] = 'x'; //operator[]函数调用:返回引用//*p在堆区,其余的都在栈区cout << &p << endl; cout << &b << endl;cout << &c << endl;cout << &(*p) << endl; cout << &s << endl;cout << (void*)&s[0] << endl; //需要强转,否则输出的是字符串int x = 1, y = 2;//右值:不可以取地址//以下的 10、x + y、fmin(x, y)、string("11111") 都是常见的右值10; //字面量常量x + y; //返回临时对象min(x, y); //返回临时对象string("1111"); //匿名对象//cout << &10 << endl;//cout << &(x + y) << endl;//cout << &(min(x, y)) << endl;//cout << &string("1111") << endl;return 0;
}
2.左值引用和右值引用
Type& r1 = x; Type&& rr1 = y;
第一个语句就是左值引用,左值引用就是给左值取别名,第⼆个就是右值引用,右值引用就是给右值取别名。- 左值引用不能直接引用右值,但是const左值引用可以引用右值。
- 右值引用不能直接引用左值,但是右值引用可以引用move(左值)。
- move是库里面的一个函数模板,本质内部是进行强制类型转换,当然他还涉及一些引用折叠的知
识。
- 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量的属性是左值。
- 语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看下面代码中r1和rr1汇编层实现,底层都是用指针实现的,可以取地址,左值引用和右值引用都是左值。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到一起去理解,互相佐证,这样反而是陷入迷途。语法是语法,低层是低层。
面试题
:左值引用和右值引用的最终目的都是减少拷贝和提高效率。左值引用还可以修改参数/返回值,方便使用。左值引用的不足之处:部分函数返回场景,只能传值返回,不能左值引用返回。当前函数的局部对象,出了当前函数作用域生命周期到了,就销毁了,不能左值引用返回,只能传值返回。
//move的低层实现
//template <class _Ty>
//remove_reference_t<_Ty>&& move(_Ty&& _Arg)
//{
// //forward _Arg as movable
// return static_cast<remove_reference_t<_Ty>&&>(_Arg);
//}//const引用:既可以传左值,也可以传右值
template<class T>
void Func(const T& x)
{}int main()
{//左值int* p = new int(10);int b = 10;const int c = b;*p = 20;string s("11111");s[0] = 'x';double x = 1.1, y = 1.1;//左值引用给左值取别名int& r1 = b;int*& r2 = p;int& r3 = *p;string& r4 = s;char& r5 = s[0];//const int& r6 = c; 这里存在权限问题,需要加上const//右值引用给右值取别名int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);string&& rr4 = string("11111");//左值引用不能直接引用右值,但是const左值引用可以引用右值const int& rx1 = 10;const double& rx2 = x + y;const double& rx3 = fmin(x, y);const string& rx4 = string("11111");//右值引用不能直接引用左值,但是右值引用可以引用:move(左值)int&& rrx1 = move(b); //注意:move后b还是左值int*&& rrx2 = move(p);int&& rrx3 = move(*p);string&& rrx4 = move(s); //move的低层就是强制类型转换string&& rrx5 = (string&&)s; //左值引用、右值引用,只要是引用,低层都是指针,可以强转//b、r1、rr1都是变量表达式,都是左值 cout << &b << endl;cout << &r1 << endl;cout << &rr1 << endl;//这里要注意的是,rr1的属性是左值,所以不能再被右值引用绑定,除非move一下 int& r6 = r1;int&& rrx6 = move(rr1); //错误写法:int&& rrx6 = rr1;//结论:左值引用的属性是左值,右值引用的属性也是左值//原因:只要是引用,低层都是指针,都可以取地址,所以是左值return 0;
}
3.引用延长生命周期
临时对象的生命周期只在当前这一行代码中。右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象的生命周期,但这些对象无法被修改。
int main()
{string s1 = "abandon";//string&& r1 = s1; //错误:右值引用不能引用左值const string& r2 = s1 + s1; //const的左值引用直接引用右值(临时对象),延长声明周期,r2销毁了,临时对象才销毁//r2 += "abandon" //错误:不能修改const的引用string&& r3 = s1 + s1; //右值引用直接引用右值,延长声明周期r3 += "abandon"; //可以修改非const的引用return 0;
}
4.左值和右值的参数匹配
- C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
- C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的fun函数,那么实参是左值会匹配fun(左值引用),实参是const左值会匹配fun(const左值引用),实参是右值会匹配fun(右值引用),总结编译器会找最匹配的调用。
- 右值引用变量在用于表达式时属性是左值,这个设计这里会感觉跟怪,讲右值引用的使用场景时,就能体会这样设计的价值了。
void fun(int& x)
{cout << "左值引用重载fun(" << x << ")\n";
}void fun(const int& x)
{cout << "const的左值引用重载fun(" << x << ")\n";
}void fun(int&& x)
{cout << "右值引用重载fun(" << x << ")\n";
}int main()
{int i = 1;const int ci = 2;fun(i); //调用fun(int&) fun(ci); //调用fun(const int&) fun(3); //调用fun(int&&)。如果没有fun(int&&),重载则会调用fun(const int&) fun(move(i)); //调用fun(int&&) //右值引用变量在用于表达式时是左值//右值引用本身的属性是左值,右值本身不能修改,但是被右值引用后就可以修改int&& x = 1;fun(x); //调用fun(int& x) fun(move(x)); //调用fun(int&& x)return 0;
}
5.右值引用和移动语义使用场景
1.左值引用使用场景
- 左值引用主要使用场景是在函数中
const左值引用传参,参数可以是右值
和左值引用传参和传返回值时减少拷贝
,同时还可以修改实参和修改返回对象
的价值。 - 左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用法传左值引用返回,如addStrings和generate函数(若返回左值引用,导致类似野指针的问题)
- 解决方案一:不用返回值,用输出型参数(牺牲了可读性)
- 解决方案二:编译器的优化(非标准,不同的编译器可能优化不同)
- 解决方案三:新标准、新语法(右值引用和移动语义)
//传值返回需要拷贝
string addStrings(string num1, string num2)
{int cur1 = num1.size() - 1, cur2 = num2.size() - 1;int carry = 0; //记录进位的值:0/1string ret;while (cur1 >= 0 || cur2 >= 0 || carry){int sum = carry; //记录两数之和if (cur1 >= 0) {sum += (num1[cur1] - '0'); //先加上第一个字符串cur1--;}if (cur2 >= 0){sum += (num2[cur2] - '0'); //再加上第二个字符串cur2--;}ret += ((sum % 10) + '0'); //尾插个位值carry = sum / 10; //更新进位值}reverse(ret.begin(), ret.end()); //逆序得到结果return ret;
}//这里的传值返回拷贝代价就太⼤了
vector<vector<int>> generate(int numRows)
{vector<vector<int>> vv(numRows);for (int i = 0; i < numRows; ++i){vv[i].resize(i + 1, 1);}for (int i = 2; i < numRows; ++i){for (int j = 1; j < i; ++j){vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];}}return vv;
}//利用输出型参数解决问题
void _generate(int numRows, vector<vector<int>>& vv);
- 那么C++11以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法改变对象已经析构销毁的事实。
2.移动构造和移动赋值
- 移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
- 移动赋值是一个赋值运算符的重载,它跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
- 对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,它的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从而提高效率。下面的xzy::string样例实现了移动构造和移动赋值,我们需要结合场景理解。
namespace xzy
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "string(char* str)-构造" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}string(const string& s):_str(nullptr){cout << "string(const string& s) -- 拷贝构造" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//移动构造string(string&& s){cout << "string(string&& s) -- 移动构造" << endl;swap(s); //右值为临时对象、匿名对象,迟早是要销毁的,不如将资源交给我,之后再销毁//右值引用的属性之所以是左值,是因为需要修改(转移资源)//如果s的属性是右值,就无法进行swap}string& operator=(const string& s){cout << "string& operator=(const string& s) -- 拷贝赋值" <<endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s){push_back(ch);}}return *this;}//移动赋值 string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}~string(){cout << "~string() -- 析构" << endl;delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];if (_str){strcpy(tmp, _str);delete[] _str;}_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity *2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}size_t size() const{return _size;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;};
}int main()
{xzy::string s1 = "123";//拷贝构造xzy::string s2 = s1;//构造+移动构造:优化后直接构造xzy::string s3 = xzy::string("123");//移动构造xzy::string s4 = move(s1); //此时s1中的资源被s4夺走了return 0;
}
3.右值引用和移动语义解决传值返回问题
1.右值对象构造,只有拷贝构造,没有移动构造的场景
- 图1展示了vs2019的debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造,右边为编译器优化的场景下连续步骤中的拷贝合⼆为一,变为一次拷贝构造。
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如图3所示。
- linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次拷贝。
图一:
2.右值对象构造,既有拷贝构造,也有移动构造的场景
- 图2展示了vs2019的debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造(效率已经很高了),右边为编译器优化的场景下连续步骤中的拷贝合二为一(注意:这里的str是左值,但是编译器将其识别成右值,进行第一次移动构造,生成临时对象,再进行第二次移动构造,生成ret对象),变为一次移动构造。
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。要理解这个优化要结合局部对象声命周期和栈帧的角度理解,如图3所示。
- linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭构造优化,运行结果可以看到图2左边没有优化的两次移动。
图二:
图三:
3.右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
- 图4左边展示了vs2019的debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
图4:
4.右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
- 图5左边展示了vs2019的debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次移动构造,一次移动赋值。
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
图5:
总结:对于深拷贝的自定义类型:如vector/string/map…,实现移动构造和移动赋值有很大的价值。对于浅拷贝的自定义类型:如Date/pair<int, int>…,不需要实现移动构造和移动赋值,此时编译器的优化有很大的价值。
6.类型分类
- C++11以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)
- 纯右值是指那些字面值常量或求值结果,相当于字面值或是一个不具名的临时对象。如: 42、true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调用,或者整形 a++,a+b 等。
- 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如move(x)、static_cast<X&&>(x)
- 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。
- 值类别-cppreference.com 和 Value categories 这两个关于值类型的中文和英文的官方文档,有兴趣可以了解细节。
- 有名字就是gvalue;有名字且不能被move的就是lvalue;有名字且能被move的就是xvalue;没有名字且能被move的就是prvalue。
7.引用折叠
- C++中不能直接定义引用的引用如
int& && r = i;
,这样写会直接报错,通过模板或 typedef 中的类型操作可以构成引用的引用。 - 通过模板或 typedef 中的类型操作可以构成引用的引用时,这时C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其它组合均折叠成左值引用。
- 下面的程序中很好的展示了模板和 typedef 时构成引用的引用时的引用折叠规则。
- 像f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,它传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用。
//由于引用折叠的限制,f1实例化之后总是一个左值引用
template<class T>
void f1(T& x)
{}//由于引用折叠的限制,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x)
{}int main()
{typedef int& lref;typedef int&& rref;int n = 0;//引用折叠lref& r1 = n; // r1的类型是 int&lref&& r2 = n; // r2的类型是 int&rref& r3 = n; // r3的类型是 int&rref&& r4 = 0; // r4的类型是 int&&//没有折叠->实例化为 void f1(int& x)f1<int>(n);f1<int>(0); //报错//折叠->实例化为 void f1(int& x)f1<int&>(n);f1<int&>(0); //报错//折叠->实例化为 void f1(int& x)f1<int&&>(n);f1<int&&>(0); //报错//折叠->实例化为 void f1(const int& x)f1<const int&>(n);f1<const int&>(0);//折叠->实例化为 void f1(const int& x)f1<const int&&>(n);f1<const int&&>(0);//没有折叠->实例化为 void f2(int&& x)f2<int>(n); //报错f2<int>(0);//折叠->实例化为 void f2(int& x)f2<int&>(n);f2<int&>(0); //报错//折叠->实例化为 void f2(int&& x)f2<int&&>(n); //报错f2<int&&>(0);return 0;
}
- Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的Function,实参是右值,实例化出右值引用版本形参的Function。
//万能引用模版:一般不需要传入T,由编译器自动推导类型。传入左值就是左值引用,传入右值就是右值引用
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); //左值//move(a)是右值,推导出T为int,模版实例化为 void Function(int&& t)Function(move(a)); //右值const int b = 1;//b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)//Function内部会编译报错,x不能++Function(b); //const 左值//move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)//Function内部会编译报错,x不能++ Function(move(b)); //const 右值return 0;
}
8.完美转发
- Function(T&& t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数。
- 但是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下一层函数Fun,那么匹配的都是左值引用版本的Fun函数。
这里我们想要保持t对象的属性,就需要使用完美转发实现。
- template < class T> T&& forward (typename remove_reference::type& arg);
- template < class T> T&& forward (typename remove_reference::type&& arg);
- 完美转发forward本质是一个函数模板,它主要还是通过引用折叠的方式实现,下面示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回。
//template <class _Ty>
//_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
//{
// // forward an lvalue as either an lvalue or an rvalue
// return static_cast<_Ty&&>(_Arg);
//}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<class T>
void Function(T&& t)
{//Fun(t); //由于右值引用的自身属性是左值,所以不管t是左值引用/右值引用,Fun函数匹配的都是左值引用Fun(forward<T>(t));
}int main()
{Function(10);int a;Function(a);Function(move(a));const int b = 1;Function(b);Function(move(b));return 0;
}
以下是链表插入时引入移动语义,减少拷贝,提高效率:
namespace xzy
{template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;左值版本//list_node(const T& data = T())// :_data(data)// ,_next(nullptr)// ,_prev(nullptr)//{}右值版本//list_node(T&& data)// :_data(forward<T>(data))// , _next(nullptr)// , _prev(nullptr)//{}list_node() = default;//万能引用template<class X>list_node(X&& data):_data(forward<X>(data)), _next(nullptr), _prev(nullptr){}};template<class T, class Ref, class Ptr>struct list_iterator{typedef list_node<T> Node;Node* _node;list_iterator(Node* node):_node(node){}};template<class T>class list{typedef list_node<T> Node;public:typedef list_iterator<T, T&, T*> iterator;iterator begin(){return _head->_next;}iterator end(){return _head;}list(){_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;}左值版本//void push_back(const T& x)//{// insert(end(), x);//}右值版本//void push_back(T&& x)//{// insert(end(), forward<T>(x)); //x一定是左值,此时需要完美转发//}//万能引用template<class X>void push_back(X&& x){insert(end(), forward<X>(x)); //x一定是左值,此时需要完美转发}左值版本//iterator insert(iterator pos, const T& x)//{// Node* cur = pos._node;// Node* prev = cur->_prev;// Node* newnode = new Node(x);// newnode->_next = cur;// cur->_prev = newnode;// newnode->_prev = prev;// prev->_next = newnode;// ++_size;// return newnode;//}右值版本//iterator insert(iterator pos, T&& x)//{// Node* cur = pos._node;// Node* prev = cur->_prev;// Node* newnode = new Node(forward<T>(x));// newnode->_next = cur;// cur->_prev = newnode;// newnode->_prev = prev;// prev->_next = newnode;// ++_size;// return newnode;//}//万能引用template<class X>iterator insert(iterator pos, X&& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(forward<X>(x));newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;++_size;return newnode;}private:Node* _head;size_t _size;};
}int main()
{xzy::list<xzy::string> lt;xzy::string s1("111");lt.push_back(s1);xzy::string s2("222");lt.push_back(move(s2));lt.push_back("333");return 0;
}
相关文章:
【C++11】列表初始化、右值引用和移动语义、引用折叠、完美转发
C11 一.C的发展历史二.列表初始化1.C98的{}2.C11的{}3.C11中的std::initializer_list 三.右值引用和移动语义1.左值和右值2.左值引用和右值引用3.引用延长生命周期4.左值和右值的参数匹配5.右值引用和移动语义使用场景1.左值引用使用场景2.移动构造和移动赋值3.右值引用和移动语…...
Openssl1.1.1s rpm包构建与升级
rpmbuild入门知识 openssh/ssl二进制升级 文章目录 前言一、资源准备1.下载openssh、openssl二进制包2.安装rpmbuild工具3.拷贝源码包到SOURCES目录下4.系统开启telnet,防止意外导致shh无法连接5.编译工具安装6.补充说明 二、制作 OpenSSL RPM 包1.编写 SPEC 文件2.…...
递归思想的深度理解——汉诺塔问题和青蛙跳台阶问题
递归的深度理解——汉诺塔问题and青蛙跳台阶问题 青蛙跳台阶问题汉诺塔问题 青蛙跳台阶问题 问题:一只青蛙可以一次跳一级台阶,也可以一次跳两级台阶,如果青蛙要跳n级台阶,共有多少种跳法? 解答:我们可以先…...
从数据到诊断:朴素贝叶斯算法助力肿瘤预测之路
1.案例概述 肿瘤性质的判断影响着患者的治疗方式和痊愈速度。传统的做法是医生根据数十个指标来判断肿瘤的性质,预测效果依赖于医生的个人经验而且效率较低,而通过机器学习有望能快速预测肿瘤的性质。 2.数据集 本次肿瘤预测使用的数据集共有569组样本…...
Element-UI:如何实现表格组件el-table多选场景下根据数据对某一行进行禁止被选中?
如何实现表格组件el-table多选场景下根据数据对某一行进行禁止被选中? 在使用 Element UI 的 Table 组件时,如果你想要禁用某一行的选中(特别是在多选模式下),可以通过自定义行的 selectable 属性来实现。selectable …...
Dexcap复现代码数据预处理全流程(四)——demo_clipping_3d.py
此脚本的主要功能是可视化点云数据文件(.pcd 文件),并通过键盘交互选择演示数据的起始帧和结束帧,生成片段标记文件 (clip_marks.json) 主要流程包括: 用户指定数据目录:检查目录是否存在并处理标记文件 -…...
JWT理解
前言 随着互联网的快速发展,身份验证和授权成为了许多应用的重要需求。JWT(JSON Web Token)作为一种轻量级的身份验证和授权机制,得到了广泛的应用。本文将为您详细介绍JWT的原理、结构和优点,帮助您更好地理解和应用…...
一种融合联邦学习和大模型特点的全新系统架构
一种融合联邦学习和大模型特点的全新系统架构 以下是一种融合联邦学习和大模型特点的全新系统架构设计: 分层分布式架构 底层 - 数据采集与预处理层:由大量的边缘设备和终端节点组成,如智能手机、物联网传感器等。这些设备负责采集本地数据,并在本地进行初步的数据预处理,…...
html表格table导出excel,主从表格式,带样式.自动分列
html的table导出成excel, vue模板 项目使用xlsx-js-style 源代码从https://github.com/gitbrent/xlsx-js-style/releases/tag/v1.2.0 下载 用里面的dist目录下的文件即可. 复制到vue项目的public目录下的XLSX目录下. 在index.hml中引入js脚本, 为啥要在这里引入? 是因为这里…...
U8G2库使用案例(stm32)
目录 一、小球在 OLED 屏幕平面内运动并碰撞反弹的效果 二、 简单的波形生成和显示程序: 三、三维三角形旋转展示 四、正方形平面内顺时针旋转 五、带有旋转点的空心圆圈应用 六、字幕滚动效果 七、下雪动画效果 八、进度条动画效果 自己移植的U8g2库,OLED库…...
067B-基于R语言平台Biomod2模型的物种分布建模与数据可视化-高阶课程【2025】
课程培训包含:发票全套软件脚本学习数据视频文件导师答疑 本教程旨在通过系统的培训学习,学员可以掌握Biomod2模型最新版本的使用方法,最新版包含12个模型(ANN, CTA, FDA, GAM, GBM, GLM, MARS, MAXENT, MAXNET, RF, SRE, XGBOOST…...
【通俗理解】AI的两次寒冬:从感知机困局到深度学习前夜
AI的两次寒冬:从感知机困局到深度学习前夜 引用(中英双语) 中文: “第一次AI寒冬,是因为感知机局限性被揭示,让人们失去了对算法可行性的信心。” “第二次AI寒冬,则是因为专家系统的局限性和硬…...
141.《mac m系列芯片安装mongodb详细教程》
文章目录 下载从官网下载安装包 下载后双击解压出文件夹安装文件名修改为 mongodb配置data存放位置和日志log的存放位置启动方式一方式二方式二:输入mongo报错以及解决办法 本人电脑 m2 pro,属于 arm 架构 下载 官网地址: mongodb官网 怎么查看自己电脑应该下载哪个版本,输入…...
【Linux】sed编辑器
一、基本介绍 sed编辑器也叫流编辑器(stream editor),它是根据事先设计好得一组规则编辑数据流。 交互式文本编辑器(如Vim)中,可以用键盘命令交互式地插入、删除或替换文本数据。 sed编辑器是根据命令处理…...
unity3d-搞个场景漫游如何实现Alpha
要处理两个问题: 如何设置地面人不掉下去 方法一、 游戏物体加刚体,将游戏物体和地面加collider。如果是地形,可以使用 Terrain Collider;如果是简单的平面,可以添加 Box Collider 或者 Mesh Collider(如果…...
概率基本概念 --- 离散型随机变量实例
条件概率&独立事件 随机变量 - 离散型随机变量 - 非离散型随机变量 连续型随机变量奇异性型随机变量 概率表示 概率分布函数概率密度函数概率质量函数全概率公式贝叶斯公式 概率计算 数学期望方差协方差 计算实例 假设有两个离散型随机变量X和Y,它们代…...
oscp备考 oscp系列——Kioptix Level 1靶场 古老的 Apache Vuln
目录 前言 1. 主机发现 2. 端口扫描 3. 指纹识别 4. 目录扫描 5. 漏洞搜索和利用 前言 oscp备考,oscp系列——Kioptix Level 1靶场 Kioptix Level 1难度为简单靶场,主要考察 nmap的使用已经是否会看输出,以及是否会通过应用查找对应漏…...
【简博士统计学习方法】3. 统计学习方法的三要素
3. 统计学习方法的三要素 3.1 监督学习的三要素 3.1.1 模型 假设空间(Hypothesis Space):所有可能的条件概率分布或决策函数,用 F \mathcal{F} F表示。 若定义为决策函数的集合: F { f ∣ Y f ( X ) } \mathcal{F…...
UnionTech OS Server 20 网页无法访问yum源地址
统信yum地址 https://euler-packages.chinauos.com/server-euler/fuyu/1060/everything/sw_64/Packages/ 浏览器访问401报错无权限,查看linux uos环境下yum配置的用户名和密码 cat /etc/yum/vars/auth_* 然后自己组装生成Basic Authorization def generate_basic_…...
WPF区域导航+导航参数使用+路由守卫+导航日志
背景:使用ContentControl控件实现区域导航是有Mvvm框架的WPF都能使用的,不限于Prism 主要是将ContenControl控件的Content内容在ViewModel中切换成不同的用户控件 下面是MainViewModel: private object body;public object Body {get { retu…...
jvm基础
jvm的基本结构 类加载器(ClassLoader):加载class文件到内存中进行使用。 运行时数据区(Runtime Data Area):这是JVM在运行Java程序期间管理的内存区域,包括方法区(Meta…...
kaggle竞赛:纽约出租车行程时间NYC Taxi Trip Duration
1.引言 作为一名(坦白说有点懒的)图像处理方向的研究生,说实话最近新开一个坑,可能是因为要寒假了比较无聊,这次带来的系列是kaggle数据处理竞赛的经典例题:纽约出租车行程时间问题。希望大家多多支持&…...
Python提取目标Json键值:包含子嵌套列表和字典
目标:取json中所有的Name、Age字典 思路:递归处理字典中直接包含子字典的情况, import jsondef find_targ_dicts(data,key1,key2):result {}if isinstance(data, dict):if key1 in data and key2 in data: # 第一层字典中包含key1和key2re…...
<div>{{ $t(“collectionPlan“) }}</div> 中的$t是什么
$t是Vue I18n插件提供的一种方法,用于根据当前应用的语言环境来获取相应的翻译文本。 以下是一个简单的示例,展示如何在Vue I18n中定义消息: const i18n new VueI18n({locale: en, // 设置默认语言messages: {en: {collectionPlan: Collec…...
医学图像分析工具01:FreeSurfer || Recon -all 全流程MRI皮质表面重建
FreeSurfer是什么 FreeSurfer 是一个功能强大的神经影像学分析软件包,广泛用于处理和可视化大脑的横断面和纵向研究数据。该软件由马萨诸塞州总医院的Martinos生物医学成像中心的计算神经影像实验室开发,旨在为神经科学研究人员提供一个高效、精确的数据…...
win32汇编环境,在对话框中画五边形与六边形
;运行效果 ;win32汇编环境,在对话框中画五边形与六边形 ;展示五边形与六边形的画法 ;将代码复制进radasm软件里,直接编译可运行.重要部分加备注。 ;下面为asm文件 ;>>>>>>>>>>>>>>>>>>>>>>>>>&g…...
小白学Pytorch
小白学Pytorch 发现一个比较好的教程,对于自己来说比较合适,适合从零开始的教程。 1、搭建一个简单的网络 https://www.cnblogs.com/PythonLearner/p/13587092.html 搭建网络这步说的比较清楚: 我们使用nn包中的Sequential搭建网络&#…...
[A-25]ARMv8/v9-GIC的系统架构(中断的硬件基础)
ver0.1 前言 我们在观看很多的影视剧过程中,尤其是军旅体裁类型的布景中,经常会看见高级干部的办公桌上都会有几部电话机。这样的电话可不能小看,重要的事情尤其是突发和紧急的情况都要通过这几部电话第一时间通知给决策者。这几部电话,必须举报几个特点:及时性好、稳定…...
毕业项目推荐:基于yolov8/yolov5的行人检测识别系统(python+卷积神经网络)
文章目录 概要一、整体资源介绍技术要点功能展示:功能1 支持单张图片识别功能2 支持遍历文件夹识别功能3 支持识别视频文件功能4 支持摄像头识别功能5 支持结果文件导出(xls格式)功能6 支持切换检测到的目标查看 二、数据集三、算法介绍1. YO…...
学习threejs,导入AWD格式的模型
👨⚕️ 主页: gis分享者 👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨⚕️ 收录于专栏:threejs gis工程师 文章目录 一、🍀前言1.1 ☘️THREE.AWDLoader AWD模型加…...
C# 事件
目录 1、事件模型的5个组成部分2、使用内置委托类型声明事件2.1 EventHandler2.1.1 ?2.1.2 this2.1.3 使用匿名函数和lamda表达式2.1.3.1 匿名函数2.1.3.2 lamda表达式 2.1.4 异常处理 2.2 EventHandler<TEventArgs> 3、使用自定义委托类型声明事件3.1 事件的…...
WebRtc02: WebRtc架构、目录结构、运行机制
整体架构 WebRtc主要分为三层: CAPI层:外层调用Session管理核心层:包括视频引擎、音频引擎、网络传输 可由使用者重写视频引擎:编解码器、视频缓存、视频增强音频引擎:编解码器、音频缓存、回音消除、降噪传输&#x…...
耐高压26V输入5V升压充电8.4V芯片
HU6877作为一款集成了26V高耐压保护的5V升压至8.4V两节锂电池充电管理IC,凭借其高效升压、智能充电管理、多重安全保护及高耐压特性,在高端手电筒、便携式医疗设备、无人机等领域展现出了广泛的应用前景。本文将详细探讨HU6877的技术特点、工作原理、应用…...
【连续学习之LwM算法】2019年CVPR顶会论文:Learning without memorizing
1 介绍 年份:2019 期刊: 2019CVPR 引用量:611 Dhar P, Singh R V, Peng K C, et al. Learning without memorizing[C]//Proceedings of the IEEE/CVF conference on computer vision and pattern recognition. 2019: 5138-5146. 本文提…...
数组和指针
数组和指针 数组初始化特殊的字符数组 指针的引入指针的定义和初始化指针操作指针和const限定符 C风格字符串 C提供了两种类似于vector和迭代器的低级复合类型:数组和指针。与vector类似,数组也可以保存某种类型的一组对象,只是数组一经创建就…...
【数电尾灯设计】2022-8-16
缘由数电尾灯设计问题,求解答--CSDN问答 从题目可以列出 000 100 010 111-----------4进制 000 100 010 110 001 101 011 111-----------8进制 由列出可知用16进制芯片的3个引脚可以获得8进制推导出4进制从而可用逻辑处理为4进制实现尾灯功能。之上第一步实现了尾灯…...
F.interpolate函数
F.interpolate 是 PyTorch 中用于对张量(通常是图像数据)进行插值操作的函数,常用于调整张量的大小,例如改变图像的分辨率。它支持多种插值方法,包括最近邻插值、双线性插值和三次插值等。 语法 torch.nn.functional…...
Tableau数据可视化与仪表盘搭建-基础图表制作
目录 对比分析:比大小 柱状图 条形图 数据钻取 筛选器 热力图 气泡图 变化分析:看趋势 折线图 预测 面积图 关系分布:看位置 散点图 直方图 地图 构成分析:看占比 饼图 树地图 堆积图 对比分析:比大…...
Sentinel-5P遥感数据下载及预处理教程【20250105】
Sentinel-5P是欧空局(Europe Space Agency,ESA)于2017年10月13日发射的一颗全球大气污染监测卫星。卫星搭载了对流层观测仪(Tropospheric Monitoring Instrument,TROPOMI),可以有效的观测全球各…...
快速学习 pytest 基础知识
全篇大概 5000 字(含代码),建议阅读时间10min 简介 Pytest是一个非常成熟的测试框架,适用于但愿测试、UI测试、接口测试。 简单灵活、上手快支持参数化具有多个第三方插件可以直接使用 assert 进行断言 一、Pytest安装 pip inst…...
[python]解决AttributeError: module ‘PIL.Image‘ has no attribute ‘ANTIALIAS‘问题
环境:python3.13.1 问题: 利用ddddocr进行验证码识别时报AttributeError: module PIL.Image has no attribute ANTIALIAS错误信息,具体如下: python3 Python 3.13.1 (main, Jan 6 2025, 22:29:09) [Clang 16.0.0 (clang-1600.…...
【C++】类和对象(下):友元、static成员、内部类、explicit 和 匿名对象
文章目录 前言一、友元二、static成员三、内部类四、隐式类型转换(加explicit可以阻止隐式类型转换)五、匿名对象 前言 一、友元(友元函数 和 友元类) 二、static成员(类中被static修饰的成员变量 和 成员函数…...
Shapelet-aeon-1
本文中,我们将首创一个长度为m、维度为d的时间序列表示为一个向量。 在aeon中,我们将一个序列描述为x:(n_channels, n_timepoints). x是一个二维数组,其中n_channels对应的是维度d,或者说是特征数量;n_timepoints对应…...
2. 进程和线程
文章目录 前言1. 进程是什么2. 进程的相关属性3. 线程是什么4. 为什么引入线程5. 进程和线程的区别 前言 上一篇博客,我们讲到了CPU和操作系统,今天我们讲一个操作系统中一个非常重要的概念—线程和进程 1. 进程是什么 每个应用程序运行于现代操作系统…...
CE中注册的符号地址如何通过编程获取
我的方式是先执行lua申请共享内存,内存名称是进程id,这样多开也不受影响,然后通过共享内存的名字就可以读到地址了。之后的人造指针的地址也都可以放这里集中管理。 -- 申请内存 local size 1024 -- 申请 1024 字节(1 KB&#…...
QT上实现SVM进行数据分类
针对不了解SVM的原理的同学强推下面这个课程: 6.机器学习课程(六)支持向量机(线性模型)问题_哔哩哔哩_bilibili 一、QT实现SVM的方法 1.调用SVM的C语言库:麻烦,要专门去找库,cmak…...
【unity调用c++动态库,c++和c#相互调用】
文章目录 前言一、编写C++动态库(MyLibrary.cpp)二、 编译C++动态库三、编写C#代码(CallbackExample.cs)四、将C++动态库与Unity项目集成五、Unity运行结果六、注意事项总结前言 在Unity中调用C++动态库并进行回调到C#的功能实现,涉及到C++与C#之间的交互。这个过程的关键…...
SAP FICO财务模块的会计年度变式是什么?有特殊的年度期间(财年)吗?可以不按日历月份来设定会计期间吗?
文章目录 一、会计准则中关于会计期间的一般性原则二、SAP系统中关于会计期间的设定(1)一般性的期间设定方式(2)特殊期间是什么?(3)有些国家与众不同的财年 三、可以不按日历月份来设定会计期间…...
Linux(17)——使用 DNF 安装和更新软件包
目录 一、使用 DNF 管理软件包: 1、 DNF 查找软件: 2、DNF 安装软件: 3、DNF 删除软件: 二、使用 DNF 管理软件包组: 1、DNF 显示组信息: 2、DNF 安装组: 三、使用 DNF 查看事务历史记录…...
vue中的设计模式
vue中使用了哪些设计模式 1. 观察者模式(Observer Pattern) 应用场景:Vue 的响应式系统核心就是观察者模式。 实现方式:通过 Object.defineProperty 或 Proxy 监听数据变化,当数据发生变化时,通知依赖的视…...