C++右值引用万能引用完美转发和引用折叠

Posted TangguTae

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++右值引用万能引用完美转发和引用折叠相关的知识,希望对你有一定的参考价值。

目录

什么是左值,什么是右值?

右值引用

万能引用

引用折叠

完美转发


什么是左值,什么是右值?

(接下来我们将左值称为 lvalue,右值成为 rvalue)

左值通常指的是变量,或者说是可以放到等号左边的表达式。右值通常是常量表达式或者函数返回值(临时对象)。

更加精简的说法是:

如果可以对一个表达式取地址,那这个表达式就是lvalue。

其他情况下,这个表达式就是一个rvalue。

举例子:

常见的左值的情况

int x=2,y=3;//x,y就为左值
++x;--y;//表达式为左值
const int& z = x;//z也为左值

右值又可以分为两种情况:纯右值将亡值

纯右值:基本类型(int,char等)的常量或者临时对象。

100,true//常量
x++,x+1//表达式

将亡值:自定义类型的临时对象 和 函数返回对象类型的右值引用

int&& fuc1(void)

    return 100;


string fuc2(void)

    string str = "hello";
    return str;

int main()

    fuc1();//函数返回值为右值引用
    fuc2();//函数返回值为临时对象


//对对象类型右值引用的转换
void fuc3()

    static_cast<int&&>(100);
    std::move(100);

表达式的左右值与类型无关!

这里有两个概念,一个是值类别,一个是值类型

值类别:对应着左值和右值的概念

值类型:数据类型。

例如:

int x = 100;

const int& y = x;

int&& z = 100;

此时x是的值类别为左值,值类型为int类型。

同理,y的值类别为左值,而值类型为const int&

注意:z的值类别也是左值,值类型为 int&&。

右值引用

正常的左值引用是无法引用右值的(常左值引用可以),所以需要右值引用去引用右值。

int main()

    int x = 1,y = 2;

    //左值引用
    int a = 0;
    int& b = a;

    //左值引用不能引用右值,const左值引用可以,因为临时变量具有常性
    const int& e = 10;
    const int& f = x + y;

    //右值引用
    int&& c = 10;
    int&& d = x + y;

    //右值引用不能引用左值,但是可以引用move后的左值
    int&& m = move(a);
    return 0;

万能引用

万能意思就和表面的意思一样,即既可以引用左值,也可以引用右值

万能引用的形式也是“&&”,所以说如何区分一个引用是右值引用还是万能引用?

总共有两种情况会出现万能引用

1、函数模板参数

template<typename T>

void fuc(T&& param); //此时为万能引用

2、auto声明

auto&& var2 = var1;//var2是一个万能引用

这两种存在共同的特点都存在类型的推导。其实这两者可以归为一类,auto声明的变量的类型推导规则本质上和模板是一样的,所以使用auto的时候你也可能得到一个万能引用。

例如:

template<typename T>
void fuc1(T&& param); //此时为万能引用

void fuc2(int&& param);//没有类型推导


int main()

	int x = 100;
	fuc1(100);
	fuc1(x);
	return 0;

因为通用引用是引用,所以必须初始化。通用引用的初始化决定了它表示的是右值引用还是左值引用。fuc1(100)中100是右值,说明通用引用被一个右值初始化。fuc1(x)中x为左值,说明通用引用被一个左值初始化。

有几种情况不要弄混淆

1、引用必须得精确!

即一定得是T&&的形式

template<typename T>
void fuc(std::vector<T>&& param);

上面这个例子不具备T&& param的格式,而是vector<T>&& param的格式,所以只是一个普通的右值引用。

template<typename T>
void fuc(vector<T>&& param); //此时不是万能引用
int main

    vector<int> v;//左值
	fuc(v);//error...
	fuc(vector<int>());//vector<int>()临时变量,右值
    
    return 0;

注意:const T&&也会使得通用引用失效。

template<typename T>
void fuc(const T&& param);//此时为右值引用

2、模板内部的函数参数为T&&的形式,注意分辨清楚!

利用vector中的 push_back和emplace_back为例来说明这个情况。

template <class T, class Allocator = allocator<T> >
class vector 
public:
	...
		void push_back(T&& x);       // fully specified parameter type ⇒ no type deduction;
	...                          // && ≡ rvalue reference
;

注意这种情况,虽然T&&是模板参数,但是此时这个参数是右值引用,原因是vector<T>这个类就已经知道T是什么类型了,此时对于void push_back(T&& x);中的T没必要再推导了,缺少万能应用推导的那一个环节。也就是说push_back依赖于vector的实例化而实例化的类型就完全决定了push_back的函数声明。可以在看一下void push_back(T&& x);在类外面是如何定义的。

template <class T>
void vector<T>::push_back(T&& x);//依赖于vector<T>类

vector模板实例化的过程:

以vector<string> vs;为例

template <class string, class Allocator = allocator<string> >
class vector 
public:
	...
		void push_back(string&& x);       // && ≡ rvalue reference
	...                          
;

push_back并没有用到类型推导,直接取决于vector的实例化。

所以这也是为什么push_back有两个版本的原因。

与此相反,vector中另一个插入的函数emplace_back就可以实现类型的推导。

template <class T, class Allocator = allocator<T> >
class vector 
public:
    ...
    template <class... _Valty>
    void emplace_back(_Valty&&... _val); // deduced parameter types ⇒ type deduction;
    ...                                // && ≡ universal references
;

类型 _Valty 是独立于模板参数 T 的,所以每次调用emplace_back时,_val就需要推导一次,这就是它比较巧妙的地方。

template<class... Args>
void std::vector<string>::emplace_back(Args&&... args);

看一下这两个的原码(msvc)

	template<class... _Valty>
		decltype(auto) emplace_back(_Valty&&... _Val)
			// insert by perfectly forwarding into element at end, provide strong guarantee
		if (_Has_unused_capacity())
			
			return (_Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...));
			

		_Ty& _Result = *_Emplace_reallocate(this->_Mylast(), _STD forward<_Valty>(_Val)...);
#if _HAS_CXX17
		return (_Result);
#else /* ^^^ _HAS_CXX17 ^^^ // vvv !_HAS_CXX17 vvv */
		(void)_Result;
#endif /* _HAS_CXX17 */
		

	void push_back(const _Ty& _Val)
			// insert element at end, provide strong guarantee
		emplace_back(_Val);
		

	void push_back(_Ty&& _Val)
			// insert by moving into element at end, provide strong guarantee
		emplace_back(_STD move(_Val));
		

可以看到在msvc下的push_back是基于emplace_back实现的。在这里,有两个比较重要的地方,一个是std::move std::forward两个函数,放在后面的完美转发再讲。


引用折叠

在这里,其实是涉及到类型的推导,也就是去推模板参数中的 T 具体是什么类型,而且是针对通用引用场景下的类型的推导。

在通用引用下类型推倒的机制很简单:当实参是左值时,T的类型为左值引用,当传右值时,T被推导为非引用类型。

例如:

template<typename T>
void fuc(T&& param); //此时为万能引用

string str = "hello";

fuc(str);//T推导为 string&
fuc(string());//T推导为string

str 为左值,所以T的类型会被推到为string&,那么param的类型就为 string& &&我们知道在C++里面引用的引用是非法的,例如int x = 100;  auto& &y = x;

所以 string& &&这该如何解释呢?

引用折叠!

程序员是不能自己声明引用的引用,但是编译器可以在特定的上下文中生成,模板的实例化就是其中之一(另外还有三种情况),编译器生成的引用的引用就会触发引用折叠。

引用折叠的情况

由于引用有两种类型:左值引用&和右值引用&&,所以两两组合总共四种情况,分别是lvalue reference to lvalue reference,  lvalue reference to rvalue reference,  rvalue reference to lvalue reference, 以及 rvalue reference to rvalue reference。

引用折叠的规则:

如果两个引用中有任何一个是左值引用lvalue reference ,那么最终的结果一定是左值引用,否则就为右值引用。

所以上面的string& &&最终得到param的类型为string&。

template<typename T>
void fuc(T&& param); //此时为万能引用

int main()

	int x = 100;
	int& lx = x;
	int&& rx = 100;

	fuc(x);//T被推导为int&
	fuc(lx);//T被推导为int&
	fuc(rx);//T被推导为int&

	return 0;

有些人会在上面的例子存在疑惑,这里的右值引用传进去为什么还是int&,其实在文章开头的时候就已经说过了,要区分值类别值类型。虽然x,lx,rx的值类型都不相同,分别为int,int&,int&&,但是他们的值类别是相同的,都是lvalue。根据上面的规则左值对应的就是类型的引用。

除此之外,还有三种情况可能导致引用折叠

auto&&

auto本身类型推导与模板类型推导基本相同

string str = "hello";
auto&& lstr = str;//等价为 string& && lstr = str;
auto&& rstr = string();//等价为 string&& lstr = string();

typedef

如下例子可以说明typedef场景也可能存在引用折叠的情况。 

template<typename T>
class myclass

	typedef T&& RvalueRfeType;
;
int main()

    myclass<int&> mc;

myclass<int&> mc;此时将int& 带入模板可以得到

typedef int& && RvalueRfeType; 可以推出RvalueRfeType为一个左值引用 int&,所以并不是想象当中的右值引用,需要注意。

decltype

这里先留个坑,等到下次详细讲auto 与decltype时在回过来填坑。


完美转发

先看个例子

void fuc(int& param)//左值引用

	cout << "lvalue reference" << endl;


void fuc(int&& param)//右值引用

	cout << "rvalue reference" << endl;


template<typename T>
void PerfectFoward(T&& param)//通用引用

	fuc(param);
;

int main()

	PerfectFoward(100);//传一个右值

	return 0;

输出结果:lvalue reference

传进来是个右值,为什么最终调用的却是fuc函数参数为左值引用 的版本呢?

捋一捋过程:

100是右值,模板类型的推导 T 为 int,然后param的类型就是 int&&,到这没问题,但是,忽略掉了此时的param为左值,那调用函数当然是去调用形参为左值引用版本的函数。

所以说,在传参的过程中,右值引用在第二次传递参数的过程中,右值属性会发生丢失,导致调用的都是左值引用的函数。

为了解决这个问题,引入完美转发

template<typename T>
void PerfectFoward(T&& param)

	fuc(forward<T>(param));//forward完美转发
;

forward的实现原理

底层源码:

template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)

    return static_cast<T&&>(param);

在这里面有一个东西 remove_reference,这个就比较有意思了。他的作用是移除T中的引用部分。也就是将T&和T&&变成T

template<typename _Tp>
struct remove_reference
 typedef _Tp   type; ;

template<typename _Tp>
struct remove_reference<_Tp&>
 typedef _Tp   type; ;

template<typename _Tp>
struct remove_reference<_Tp&&>
 typedef _Tp   type; ;

这里其实也是模板推导的一些知识(不懂得可以去看一下effective modern c++里面的条款1),正因为如此才可以把T里面的引用给去除掉。

(C++真的是语法让人又爱又恨,不得感叹有意思,但是就很麻烦)

在头文件<type_traits>中有各种各样的模板完成这种需求的转换工作,把这些叫做模板元编程(TMP),抱歉我不是一个牛叉的C++程序员。我要走的路还很长。

我们回过头来再来看看forward 的实现。

假设我们现在给一个左值,int x = 100。

template<typename T>
void PerfectFoward(T&& param)

	fuc(forward<T>(param));//forward完美转发
;
int x = 100;
PerfectFoward(x);

此时T的类型为int&,并将int& 传递给forward。再来看forward这里

int& && forward(typename std::remove_reference<int&>::type& param)

    return static_cast<int& &&>(param);

remove_reference<int&>::type 会去除掉int& 中的&部分,所以返回值为int,那么引用折叠完之后最终得到的是:

int& forward(int& param)

    return static_cast<int&>(param);

static_cast返回值为一个左值引用,此时static_cast啥也没做,保留了输入参数的左值属性

当传递一个右值的时候

template<typename T>
void PerfectFoward(T&& param)

	fuc(forward<T>(param));//forward完美转发
;
PerfectFoward(100);

 此时模板参数T被推导出为int,那么forward这里

int&& forward(int& param)

    return static_cast<int&&>(param);

此时static_cast的返回值为int&&,在这篇文章开头讲过,函数返回值的右值引用是一个右值,转换成功,将一个左值param转换为一个右值,保留了最开始输入参数的右值属性

与forward对应的是move函数

move函数就很直接,直接把任何类别的对象都转化为右值。

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); 

可以看到,不管怎样static_cast返回值都为右值。

注意move是一个掠夺的机制,所以需要小心,针对右值是没什么问题的,但是针对左值时就需要小心,effective modern c++中建议,对右值使用 move,对通用引用使用forward,根据前面所描述的,想必你们也想到了在通用引用中用move可能会存在什么问题。

讲了这么多,那么右值引用有什么用?

我认为移动构造移动赋值

class String

public:
	String(const char* str = " ")
	
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	
	String(const String& s)//左值版本
	
		cout << "String(const String& s)-深拷贝" << endl;
		_str = new char[strlen(s._str) + 1];
		strcpy(_str, s._str);
	
	~String()
	
		delete[] _str;
	
private:
	char* _str;
;
String fuc(const char* str)

	String tmp(str);
	return tmp;//返回的是临时对象

int main()

	String s1("hello world");
	String s2(s1);//参数是左值
	String s3(fuc("临时对象-右值"));//临时对象,参数是右值-将亡值,用完就析构了
	return 0;

 

对于这个右值,而且是将亡值,没有必要进行深拷贝,用完直接析构,所以考虑移动拷贝

    String(String&& s)//右值,将亡值
        :_str(nullptr)//把空值交换,进行析构就没问题,随机值析构很危险
    
        cout << "String(const String&& s)-移动拷贝" << endl;
        swap(_str, s._str);
    

直接将空间进行交换,这样效率高。

 

参考的文章

现代C++之万能引用、完美转发、引用折叠 - 知乎 (zhihu.com)

《effective modern c++》

以上是关于C++右值引用万能引用完美转发和引用折叠的主要内容,如果未能解决你的问题,请参考以下文章

C++右值引用万能引用完美转发和引用折叠

完美转发与底层原理剖析(引用折叠)

完美转发与底层原理剖析(引用折叠)

C++中的万能引用和完美转发

学习C++完美转发,这篇就足够了

C++11 ——— 右值引用和移动语义