C++右值引用与移动语义
1.完美引用(std::forward<>)
在引入完美转发前先分析以下代码
#include<iostream>
using std::cout; using std::endl;
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 Perfect(T&& t)
{
Fun(t);
}
int main()
{
Perfect(1);//右值
int a = 5;
Perfect(a);//左值
Perfect(std::move(a));//右值
const int b = 10;
Perfect(b);//const 左值
Perfect(std::move(b));//const右值
return 0;
}
注意:
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。
代码运行结果如下

根据运行结果可以推测:
右值在函数传参的时候丢失了右值属性,变成了左值
右值传参时退化为左值原因
左值与右值最明显的区别是:右值不可以取地址,左值可以
右值在传参的时候被保存到了特定的位置,所以就可以取地址了,失去了右值属性。
eg:如上代码,右值被保存到了模板参数t中,可以取地址
改进方法,使用std::forward<>来保证右值属性的传递(完美转发)
#include<iostream>
using std::cout; using std::endl;
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 Perfect(T&& t)
{
Fun(std::forward<T>(t));
}
int main()
{
Perfect(1);
int a = 5;
Perfect(a);
Perfect(std::move(a));
const int b = 10;
Perfect(b);
Perfect(std::move(b));
return 0;
}
运行结果:

综上可知:只要右值传参,就一定要使用完美转发
2.模板的可变参数
template <class ...Args>
void Function(Args... args){}
Args:是模板参数包。
args:函数形参参数包
声明Args… args这个参数包中可能有0-任意个参数
方法一递归展开参数包的方法如下:
#include<iostream>
using std::cout; using std::endl;
void FunctionArg() { cout << endl; }
template <class T,class ...Args>
void FunctionArg(T value,Args... args)
{
cout << "参数个数为:" << sizeof...(args) << endl;
cout << value << endl;
FunctionArg(args...);
}
template <class ...Args>
void Function(Args... args)
{
FunctionArg(args...);
}
int main()
{
Function(std::string("sort"), 1, 'A');
Function(1, 3, 2.34);
}
运行结果:

分析:
Function函数中调用FunctionArg函数
在FunctionArg函数中,参数包被展开成value+args。
这样逐层递归,最后参数包args没有参数时要退出递归。所以上述函数要提供一个无参的函数。
注意:模板可变参数并不支持Args[i]来找参数
同时模板在展开参数时也不支持if语句。
因为if与else是逻辑代码,编译器在展开参数包的时是编译过程,模板在推演类型。同理也可以解释Args[i]型找参数
方法二:逗号表达式展开参数包(列表初始化)

如上图,编译器在编译时会计算数组的大小,参数包如果有4个参数,数组大小就为4。
如果参数都是整形,数组内的元素就是参数。

C++数组中元素类型相同,为了保证参数包参数类型结果都是整形,这里使用了逗号表达式。
eg:(“abc”,1)这个表达式最后结果为整形,综上,上述代码改进为
#include<iostream>
using std::cout; using std::endl;
template<class T>
void Print(T t)
{
cout << t << " ";
}
void Function() { cout << endl; }//匹配0个参数的情况
template <class ...Args>
void Function(Args... args)
{
//列表初始化
int arr[] = { (Print(args),0)... };
}
int main()
{
Function();
Function(1, 2, 3, 4);
cout << endl;
Function(1, "Hello", 3.14);
}
如上图列表初始化会将参数包展开为:
{(Print(args), 0)…}将会展开成((Print(arg1),0), (Print(arg2),0), (Print(arg3),0), etc… )}
这些表达式最后的结果都是整形0,可以放到数组中
同理,还可以
#include<iostream>
using std::cout; using std::endl;
template<class T>
int Print(T t)
{
cout << t << " ";
return 0;
}
void Function() { cout << endl; }
template <class ...Args>
void Function(Args... args)
{
//列表初始化
int arr[] = { Print(args)... };
}
int main()
{
Function();
Function(1, 2, 3, 4);
cout << endl;
Function(1, "Hello", 3.14);
}
3.C++STL中emplace_back)
以list中的emplace_back举例子

注意这里的void emplace_back (Args&&... args);中&&是万能引用,可以引用左值或右值。
在列表的末尾插入一个新元素。这个新元素可以使用参数包作为其构造的参数来构造。
构造时使用std::allocator_traits::construct来构造

std::allocator_traits::construct可以理解为C++定位new。
emplace_back的使用
#include<iostream>
#include<list>
using namespace std;
class Date
{
private:
int year; int month; int date;
public:
Date(int _year = 1, int _month = 1, int _date = 1)
:year(_year), month(_month), date(_date)
{}
};
int main()
{
std::list<Date> lt;
Date d1 = { 2022, 2, 2 };
lt.push_back(d1);
lt.push_back(Date(2022,2,2));
lt.push_back({2022,2,2});//列表初始化
lt.emplace_back(d1);
lt.emplace_back(Date(2022, 2, 2));
lt.emplace_back(2022, 2, 2);
return 0;
}
首先,emplace_back是可变参数的万能引用。拿到参数包时调用construct,调用时采用完美转发保证参数包内参数属性

综上:

测试emplace_back
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<list>
using namespace std;
namespace NUC
{
class string
{
public:
string(const char* str = "")
:size(strlen(str))
, capacity(size)
{
cout << "构造函数" << endl;
_src = new char[capacity + 1];
strcpy(_src, str);
}
~string()
{
delete[] _src;
_src = nullptr;
}
string(string&& s)
:_src(nullptr), size(0), capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
this->swap(s);
}
void swap(string& s)
{
::swap(_src, s._src);//全局swap std::swap
::swap(size, s.size);
::swap(capacity, s.capacity);
}
string(const string& s)
:_src(nullptr)
{
cout << "拷贝构造函数" << endl;
string tmp(s._src);
swap(tmp);
}
string& operator=(const string& s)
{
cout << "operator=" << endl;
string tmp(s);
swap(tmp);
return *this;
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
char* begin() { return _src; }
char* end() { return _src + size; }
void reserve(size_t i)
{
if (i > capacity)
{
char* tmp = new char[i + 1];
strncpy(tmp, _src, size);
delete[]_src;
_src = tmp;
capacity = i;
}
}
void push_back(char ch)
{
if (size == capacity)
{
size_t newcapacity = capacity == 0 ? 4 : capacity * 2;
reserve(newcapacity * 2);
}
_src[size] = ch;
_src[size + 1] = '\0';
size++;
}
private:
char* _src;
int size;
int capacity;
};
}
int main()
{
list<std::pair<int,NUC::string>>lt;
pair<int, NUC::string>s(1, "Hello");
lt.emplace_back(s);//调用构造,再调用拷贝构造
lt.emplace_back(pair<int, NUC::string>(1, "Word"));//调用构造函数,再调用移动构造
lt.emplace_back(1,"hello");//直接调用构造函数初始化
return 0;
}
运行结果为:



本文探讨了C++中的右值引用及其在函数参数传递时的退化问题,通过完美转发std::forward解决。接着介绍了模板的可变参数,展示了两种展开参数包的方法。最后讲解了STL中emplace_back的用法,特别是如何利用模板和完美转发在列表末尾高效地插入元素。通过实例展示了emplace_back如何避免不必要的拷贝和移动,提高性能。
594

被折叠的 条评论
为什么被折叠?



