C++ 右值引用、copy&swap 、std::move 、完美转发、std::forward

越行勤 758 2022-03-13

引用

在目前编程语言中,一般变量类型可以分类为

  1. 值类型
  2. 指针类型
  3. 引用类型

目前很多语言中只有引用类型,比如java出了基本类型以外都是引用类型,但是C+ + 都用这三种类型,所以C+ +在这方面要复杂不少,而且出现了右值引用这个概念。

左右值

C+ +可以将变量分为左值和右值,简单的说C+ +左值就是具有地址的值,右值是不具有地址的值。如何区分他们呢。

通常情况下:

  1. 常类字面量是右值 如 111, “hello world”
  2. 函数返回值,返回左值引用除外
  3. 构造无名对象

我们发现这些类型都是 临时的,即将消亡的,生命周期很短暂,所以可以成为将亡值;同时没有地址,不可以被取地址,更不可被左值引用;右值还有一个特点就是无法修改

std::string getHello()
{
	return {"Hello, World!"};
}

int main()
{
	std::cout << "Hello, World!" << std::endl;//右值
	std::cout << getHello() << std::endl;//右值
	std::cout << std::string("Hello, World!") << std::endl;//右值
	return 0;
}

为什么需要右值引用

本篇都是用构造函数举例,但是实际上不仅仅只有构造函数需要右值.

存在一个buffer类

class buffer
{
    //.....
private:
	int capacity;//容量
	int len;//长度
	unsigned char* buf;
}

类似与一个简化版本的 vector. 分析如下

函数传入参数的的时候,我们可以选择 值传递和引用传递,值传递有一个致命的缺点,就是会发生一次拷贝构造。解决办法就是加入引用

void dothing(buffer b) // buffer&
{

}
int main()
{
	buffer buf(5);
	dothing(buf);
	return 0;
}

但是就会但是另一个问题,右值是无法被引用的!也就说这个函数无法接受右值。

void dothing(buffer& b);
dothing(buffer(10)); //报错

为了让我们这个参数接受右值,我们有一个解决办法就是 const 修饰

void dothing(const buffer& b)

好了,现在我们的函数既可以接受左值,同时可以接受右值。

为啥被const修饰之后就可以接受右值了呢?

生命周期的角度去看

由于右值是将亡值,无法被左值引用,C++有一个特性,const的左值引用遇到 一个将亡值,会将将亡值的生命周期延长为这个引用的生命周期的长度。

也就说 const 左值引用是可以 接受右值的

虽然我们可以可以既可以做到接受右值,也可以做到接受左值,但是这并无法满足所有情况。比如这样我们无法在函数体内部修改 const 引用,另外右值既然是将亡值,那么他们的资源即将就要被销毁,我们是否可以利用这个资源呢?

一个典型的例子就是copy构造, 我们发现深拷贝一个对象开销不小的,但是如果是右值的传入,我们也许不需要重新开辟控件和copy数据,反正都要将亡,那么我们直接占用就好了,很可惜我们无法修改const 引用哇,所以c++添加右值引用规则

	buffer(const buffer & other)
	{
		std::cout<<"拷贝构造\n";
		this->capacity = other.capacity;
		this->len = other.len;
		this->buf = new unsigned char[capacity] {};
		std::copy(other.buf, other.buf + other.capacity, this->buf);
	}

右值的好处

	buffer(buffer && other)
	{
		std::cout<<"move拷贝构造\n";
		this->capacity = other.capacity;
		this->len = other.len;
		this->buf = other.buf;
		other.buf= nullptr;
	}

对比左值的copy 这样优化了不少。

copy & swap

一个类 如果我们需要 拷贝和复制,那么我们需要写出如下版本的函数。

如果我们不需要,使用delete 阻止编译器给我们添加默认的版本

buffer& operator=(buffer const& other) = delete;

  1. 拷贝构造函数 buffer(const buffer & other)
  2. 移动构造函数 buffer(buffer && other)
  3. 拷贝赋值函数 buffer& operator=(const buffer& other)
  4. 移动拷贝赋值函数 buffer& operator=(buffer&& other)

而且代码重复了,那么我们有没有一个简单的方法呢?

swap & copy 模式,可以减少一个函数的代码,我们声明了一个swap函数,很简答就是为了交换俩个buffer。

	buffer(buffer& buffer)
			:capacity(buffer.capacity), len(buffer.len),
			 buf(capacity ? new unsigned char[capacity] {0} : nullptr)
	{
		if(capacity)
			std::copy(buffer.buf, buffer.buf + buffer.capacity, this->buf);
	}
	buffer& operator=(buffer other)noexcept //这里时值传递  无论左值 右值都可以接收
	{
		Swap(*this, other);
		return *this;
	}
	buffer(buffer&& buffer):noexcept :capacity(0), len(0), buf(nullptr)
	{
		Swap(*this, buffer);
	}
	static void Swap(buffer& lhs, buffer& rhs)noexcept
	{
		std::swap(lhs.buf, rhs.buf);
		std::swap(lhs.capacity, rhs.capacity);
		std::swap(lhs.len, rhs.len);
	}

值得说明的是 buffer& operator=(buffer other)noexcept

传入是左值:

  1. 调用一次拷贝构造,这正是我们想要的,代替了手动copy
  2. swap霸占

传入右值:

  1. 调用一次move拷贝构造,注意不是拷贝构造函数,移动构造仅仅swap
  2. swap霸占
  3. 也只是多了一次交换过程,所以称不上逆向优化

std::move

我们在在代码中经常遇到一个需求,我们知道这个对象即将不会被使用了,我们需要将左值变为右值,std::move 就一个将一个左值 显示变为右值。

  template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

//typename std::remove_reference<_Tp> 是去掉 引用的 萃取器

//简化
	template<typename _Tp>
	_Tp&& move(_Tp&& __t) noexcept
	{
		return static_cast<_Tp&&>(__t);
	}

static_cast C++的类型转换

完美转发

引入了新的特性,但是总会存在新的需求,这就是所谓 问题越解决越多,熵是不可以减少的。

解释一下什么是完美转发,它指的是函数模板(泛型编程中遇到的问题)可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美**,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变**。

template<typename T,typename Arg>
std::shared_ptr<T> make_shared_prt(Arg arg) 
{
	return std::shared_ptr<T>(new T(arg));
}

我们调用make_shared_prt函数,就和直接调用return std::shared_ptr<T>(new T(arg))的感觉一样,这就是完美转发。

一般转发

为啥需要保证被转发参数的左、右值属性不变,因为T的构造器可能右多个版本,我们希望我们make函数可以更具我们传递的值自动调用对应版本的构造函数.但是一般情况下可能吗?

就拿我们这个例子来说, 即使我们重载了一个右值的版本

template<typename T,typename Arg>
std::shared_ptr<T> make_shared_prt(Arg&& arg) {
	return std::shared_ptr<T>(new T(arg))
}

但是我们调用 T的构造器的时候, arg是一个左值, 因为在函数内部他已经具有一个名字了.

显然一般情况下,我们是完成我们上述的完美转发的需求的

引用折叠

//T& & -> T&
//T& && -> T&
//T&& && -> T&
//T&& && -> T&&

只有 && && 才能得到 && ,下面可以拿到编译去做一个实验

	using T=int;
	using T1=T&;
	using T2=T&&;

	using T3=T1&; //T& & -> T&
	using T4=T1&&;//T& && -> T&
	using T5=T2&;//T&& && -> T&
	using T6=T2&&;//T&& && -> T&&

万能引用

前面我说让C++一个函数既可以接受左值,也可以接受右值, 可以使用const& ,其实还有一个办法,使用万能引用.

template<typename T>
void fun(T&& t)
{
	std::cout<<"万能引用\n";
}

	int i=0;
	fun(1);
	fun(i);

所谓万能引用就是既可以接受左值,同时可以接受右值. 这个必须是模板形式的 T&&,如果不是模板那么不能说是万能引用.到这个现象的原因是引用折叠.

  1. 如果我们传入左值 , 那么模板T会被展开为 T&

  2. 如果是右值 ,那么 模板T汇编展开为T&&

从而能够接受左值和右值,

std::forward

如何使用 :

template<typename T,typename Arg>
std::shared_ptr<T> make_shared_prt(Arg&& arg) {
	return std::shared_ptr<T>(new T(std::forward<Arg>(arg)));
}

这样我们就可以让 arg 被转发参数的左、右值属性不变。

原理是什么呢 ,看源码:

//两个模板  
template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
		    " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }
·
//化简
//匹配左值
//std::remove_reference<_Tp> 去掉引用的萃取器
  template<typename _Tp>
    _Tp&&
    forward(_Tp& T) noexcept
    { return static_cast<_Tp&&>(__t); }
//匹配右值
  template<typename _Tp>
    _Tp&&
    forward(_Tp&& T) noexcept
    { return static_cast<_Tp&&>(__t); }

当传入左值的时候

//Tp 展开为 Tp&

  template<typename _Tp>
    _Tp&& //-> Tp&& & -> tp&
    forward(_Tp& T) noexcept
    { return static_cast<_Tp  && 
        //-> Tp&& & -> tp&
        >(__t); }

当传入右值

//Tp 展开为 Tp&&
template<typename _Tp>
    _Tp&& //-> Tp&& && -> tp&&
    forward(_Tp& T) noexcept
    { return static_cast<_Tp&& 
        //-> Tp&& && -> tp&&
        >(__t); }