【C++】C++11之右值引用

文章目录

    • 右值引用的概念
    • 左值和右值
    • 左值引用和右值引用
    • 右值引用的使用场景
      • 左值引用的短板
      • 移动语义
        • 移动构造
        • 移动赋值
      • 在STL中的应用
      • 给中间临时变量取别名
      • 完美转发(forward)
    • 新增的默认成员函数
    • 可变参数模板
      • 可变参数包的展开
        • 递归方式展开参数包
        • 数组列表初始化方式展开参数包
      • emplace_back

右值引用的概念

以前使用的引用的概念,都是指左值引用,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。

为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用

int Add(int a, int b)
{
	return a + b;
}
int main()
{
	const int&& ra = 10;
	// 引用函数返回值,返回值是一个临时变量,为右值
	int&& rRet = Add(10, 20);
	return 0;
}

为了与C++98中的引用进行区分,C++11将该种方式称之为右值引用


左值和右值

左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能够取地址的称为左值只能放在=右边的,或者不能取地址的称为右值,但是不一定完全正确

关于左值和右值的区分不是很好区分,一般认为:

左值 右值
特点 可以被取地址 | 一般情况下可以修改(const修饰不能修改) 不能取地址 | 不能出现在 = 左边,不能被修改
例子 变量名或者解引用的指针 C语言中的纯右值,比如:a+b, 100 | 将亡值,比如:表达式的中间结果、函数按照值的方式进行返回

左值引用和右值引用

前面学过的引用是左值引用,引用就是给变量取别名,那么左值引用就是给左值取别名,右值引用就是给右值取别名

左值引用 右值引用
表示 变量后面加&表示左值引用 变量后面加&&表示右值引用
一般情况 左值引用只能引用左值,不能引用右值 右值引用只能引用右值,不能引用左值
特殊情况 const左值引用可以引用左值,也可以引用右值 右值引用可以引用move后的左值

【C++】C++11之右值引用_第1张图片

【C++】C++11之右值引用_第2张图片

左值std::move后就会转移对象,此时对象的资源就会被转移走不能再使用被move后的对象


右值引用的使用场景

左值引用的短板

左值引用可以做参数

也可以做返回值

(用我们自己实现的string类来做演示)
mystring.h


左值引用做参数可以完美解决传参时的拷贝问题

  • 先用传值传递string对象到函数

【C++】C++11之右值引用_第3张图片

可以看出在传参的时候发生了深拷贝

  • 传左值引用的方式传参

【C++】C++11之右值引用_第4张图片

传参过程并没有发生深拷贝


左值引用做返回值也可以减少拷贝

但是,当函数的返回值出了函数作用域就会销毁时,就不能使用左值引用进行返回了,此时就会发生拷贝

比如说to_string方法的返回值就是一个临时对象出了函数作用域就会销毁。所以只能使用传值返回,就会发生深拷贝

【C++】C++11之右值引用_第5张图片

在这种情况下,本来是要发生两次拷贝构造,但是在一个调用中,连续的构造一般会被编译器优化成一次


移动语义

为了解决临时对象的返回值传参深拷贝问题,就用移动语义来解决这个问题

移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题。

引入右值引用,并不是直接使用右值引用减少拷贝,提高效率。而是深拷贝的类提供移动构造移动赋值来解决返回值深拷贝问题。

引入移动语义,这些类的对象进行传值返回或者参数为右值时,可以用移动构造和移动赋值,转移资源,避免深拷贝,提高效率


移动构造

对于上面左值引用的短板,我们在深拷贝的类里实现一个移动构造,所谓移动构造,就是把传入的右值资源全部移动到当前对象当前对象不去开辟新的空间拷贝构造,而是直接使用传入的右值引用对象的资源,减少了拷贝。

// 移动构造
string(string&& s)
    :_str(nullptr)
        , _size(0)
        , _capacity(0)
    {
        std::cout << "string(string&& s) -- 移动构造" << std::endl;
        //直接交换传入右值引用的资源到当前对象
        this->swap(s);
    }

void swap(string& s){
    std::swap(_str, s._str);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
}

有了移动构造,在上面to_string方法中进行返回时,不会去拷贝构造临时对象,然后再把临时对象拷贝构造给ret

而是进行移动构造,构造一个临时对象,然后再把临时对象移动构造给ret期间没有进行深拷贝,而是进行资源的转移提高了效率

【C++】C++11之右值引用_第6张图片

【C++】C++11之右值引用_第7张图片


移动赋值

如果出现了下列场景

int main(){
    
	mystring::string ret;
	ret = mystring::to_string(1234);

	return 0;
}

对ret进行赋值操作

此时如果没有移动赋值,那么就会调用普通的赋值

// 赋值重载
string& operator=(const string& s){
    string tmp(s);
    swap(tmp);

    return *this;
}

普通的赋值会先拷贝构造一个临时对象,然后把临时对象的资源交换给当前对象,最后临时对象出作用域销毁,期间会发生一次拷贝构造

这时给这个类实现一个移动赋值

// 移动赋值 
string& operator=(string&& s){
    std::cout << "string& operator=(string&& s) -- 移动赋值" << std::endl;
    this->swap(s);

    return *this;
}

此时再用to_string方法的返回值对ret进行赋值时,就会发生以下场景

【C++】C++11之右值引用_第8张图片

这里会发生一次移动构造和一次移动赋值,由于不是一个操作,所以编译器无法进行优化

【C++】C++11之右值引用_第9张图片


在STL中的应用

右值引用还可以在使用容器插入接口函数中,如果实参是右值,那么插入就会匹配右值引用版本插入函数转移右值的资源,而不是拷贝构造,减少拷贝,提高效率

比如说下面的使用场景

int main(){

	std::list<mystring::string> v;
	mystring::string s1("111"); 
	v.push_back(s1);

	v.push_back("2222");
	v.push_back(std::move(s1));

	return 0;
}

如果插入传入的是s1,那么会调用push_back的左值引用版本s1会被拷贝构造插入到v中

如果传入的是一个右值,或者经过move后的s1,那么push_back会调用右值引用版本,右值会被直接移动到v里,不会发生拷贝构造

【C++】C++11之右值引用_第10张图片

【C++】C++11之右值引用_第11张图片


给中间临时变量取别名

右值引用的作用有一个就是给中间临时变量取别名

int main(){
    string s1("hello");
    string s2(" world");
    string s3 = s1 + s2;   // s3是用s1和s2拼接完成之后的结果拷贝构造的新对象
    stirng&& s4 = s1 + s2; // s4就是s1和s2拼接完成之后结果的别名
    return 0;
}

完美转发(forward)

完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数

对于模板参数中的&&不仅仅是引用右值,语法规定该中情况为万能引用既能引用右值也能引用左值

但是当右值引用的对象作为实参传递时,属性会退化为左值只能匹配左值引用

就比如下列场景

void Fun(int& x) { std::cout << "左值引用" << std::endl; }
void Fun(const int& x) { std::cout << "const 左值引用" << std::endl; }
void Fun(int&& x) { std::cout << "右值引用" << std::endl; }
void Fun(const int&& x) { std::cout << "const 右值引用" << std::endl; }

template<typename T>
void PerfectForward(T&& t){
    //万能引用,可以接收左值和右值
    //但是当接收的t是右值时,再次向Fun传递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;
}

【C++】C++11之右值引用_第12张图片

因为t接收右值的传参后,t有了空间用来保存传来的右值t就退化成了左值,存在属性的混淆

如果想让传给Fun的t为右值,就需要在传参的时候进行完美转发

template<typename T>
void PerfectForward(T&& t){
    //把t进行完美转发,这时Fun传入的就是保持了右值属性的t
	Fun(std::forward<T>(t));
}

【C++】C++11之右值引用_第13张图片

此时就能保持参数的右值属性


新增的默认成员函数

C++11之前的C++类中,有6个默认成员函数

  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 拷贝赋值重载
  • 取地址重载
  • const 取地址重载

C++11新增了两个:移动构造函数移动赋值运算符重载

关于移动构造函数和移动赋值,有以下生成规则

  • 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造
  • 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值
  • 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(和默认移动构造完全类似)
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值

例如下列的类,如果实现了析构函数 、拷贝构造、拷贝赋值重载中的任意一个,就不会生成默认的移动语义

class Person{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}
	Person& operator=(const Person& p){
		if (this != &p){
			_name = p._name;
			_age = p._age;
		}
		return *this;
	}
	~Person()
	{}
private:
	mystring::string _name;
	int _age;
};
int main()
{
	Person s1{ "zhangsan", 18 };
	Person s2 = s1;
	Person s3 = std::move(s1);
	Person s4;
	s4 = std::move(s2);
	return 0;
}

【C++】C++11之右值引用_第14张图片

这时我们把析构函数 、拷贝构造、拷贝赋值重载全部屏蔽,那么这个类就会自动生成移动语义,默认生成的移动语义会调用到string的移动语义,实现这个类的移动构造移动赋值

class Person{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	/*Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}
	Person& operator=(const Person& p){
		if (this != &p){
			_name = p._name;
			_age = p._age;
		}
		return *this;
	}
	~Person()
	{}*/
private:
	mystring::string _name;
	int _age;
};
int main(){
	Person s1{ "zhangsan", 18 };
	Person s2 = s1;
	Person s3 = std::move(s1);
	Person s4;
	s4 = std::move(s2);
	return 0;
}

【C++】C++11之右值引用_第15张图片


可变参数模板

C++98/03,类模版和函数模版中只能含固定数量的模版参数

C++11新特性的可变参数模板能够创建可以接收可变参数的函数模板和类模板

可变参数函数模板示例:

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数
template <class ...Args>
void ShowList(Args... args)
{}

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数

我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数

由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值


可变参数包的展开

我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数

递归方式展开参数包
//停止函数
void ShowListArg(){
	std::cout << std::endl;
}

//展开函数
template <class T, class ...Args>
void ShowListArg(T value, Args... args) {
	std::cout << value << " ";
    //递归展开
	ShowListArg(args...);
}

//解析函数
template <class ...Args>
void ShowList(Args... args) {
	ShowListArg(args...);
}

int main() {
	ShowList();
	ShowList(1, 2);
	ShowList(1, 2, "num", 3);

	return 0;
}

【C++】C++11之右值引用_第16张图片


数组列表初始化方式展开参数包

数组在实例化之前,会逐一获取到初始化列表中的参数,利用这一特性,可以把参数包的展开交给数组

template <class T>
int PrintArg(T t) {
	std::cout << t << " " ;
	return 0;
}

//展开函数
template<class ...Args>
void ShowList(Args ...args) {
	int arr[] = { PrintArg(args)... };
	std::cout << std::endl;
}

//匹配参数为0的情况
void ShowList()
{}

int main() {
	ShowList();
	ShowList(1, 2);
	ShowList(1, 2, "num", 3);

	return 0;
}

emplace_back

C++11在STL中加入了一个接口,emplace_back,功能和push_back基本一致,都是往容器里插入数据

template <class... Args>
void emplace_back (Args&&... args);

emplace_back和push_back有一些不同

  • emplace系列的接口支持模板的可变参数,并且是万能引用
  • 万能引用则能够直接拿到参数对象,以便构造类型需要的参数类型
  • 支持模板的可变参数能够让emplace通过对参数列表的展开进行一个个获取参数,并通过定位new来构造对象不需要进行额外拷贝
int main(){
	//带有拷贝构造和移动构造的mystring::string
	std::list< std::pair<int, mystring::string> > mylist;

	std::cout << "左值插入----------------------" << std::endl;
	std::pair<int, mystring::string> kv(20, "sort");
	mylist.emplace_back(kv);
	std::cout << "右值插入----------------------" << std::endl;
	mylist.emplace_back(std::move(kv));
	std::cout << "可变参数包插入-----------------" << std::endl;
	mylist.emplace_back(10, "sort");
	return 0;
}

【C++】C++11之右值引用_第17张图片

所以说emplace_back的优势在于,如果传参时传入的是可变参数包,那么在构造对象时会直接构造,不会拷贝对象,相比push_back有一定的效率提升

但是如果仅仅是传左值或者传右值对象,那么两者的效率是一样的。

你可能感兴趣的:(C++,c++,开发语言)