C++ 引用折叠、万能引用与右值引用

从C++11开始,新加入了右值 的概念,这里先简单介绍一下左值、右值及其引用的概念。

  • 左值:非临时的(具名的、可以被取地址)。可以出现在 = 号左边或右边。
  • 右值:临时的、不可以被取地址的,只能出现在 = 号的右边。
  • 左值引用:对左值的引用就是左值引用,形式为 int& lref
  • 右值引用:对右值的引用就是右值引用, 形式为 int&& rref

[!NOTE]

注:常量左值引用 是“万能”的引用类型,可以绑定到所有类型的值,包括非常量左值、常量左值、非常量右值和常量右值。

通常来讲右值是临时对象,声明周期短暂,一般在执行完当前这条表达式之后就被释放了。

通过将其赋值给右值引用,可以对其 “续命”,使其生命周期与右值引用类型变量的生命周期一样长。


由于C++11对右值、右值引用等概念的引入以及原来就存在的左值和左值引用等概念,延伸出了 引用折叠、通用引用(万能引用)等概念或者说特性。

引用折叠

我们把 引用折叠 拆解为 引用折叠 两个短语来解释。

引用:如文章开头部分所讲,是某个对象的别名,可以绑定左值或者右值。

折叠:所谓折叠意思是 C++ 不允许出现引用的引用。而在一些场景下,比如函数模板参数中,参数推导完之后,可能会出现引用的引用,这个时候引用折叠规则会 将多个引用折叠为单个引用

引用折叠具体规则如下:只要其中一个是左值引用结果就是左值引用,否则才是右值引用 。

不同组合情况声明类型折叠类型
引用的引用& &&
右值引用的引用&& &&
引用的右值引用& &&&
右值引用的右值引用&& &&&&

万能引用

万能引用(通用引用)是在 参数推导中(模板参数推导(T&&) 或者 auto&&推导),利用引用折叠的相关规则,使其既可以绑定左值引用,又可以接收右值引用,并且保持左右值const属性的引用类型,形如 T&& 或者 auto&&

万能引用的两个关键点👀:

  • 必须涉及参数类型推导(T&& 或者 auto&&)
  • 必须严格形如 T&& 或者 auto&& , 比如如果函数模板参数是 const T&&,就不是万能引用。

万能引用允许等号右侧是任意合法的表达式,而等号左侧总是可以根据表达式类别,推演出合适的引用类型。

template <typename T>
void MyFunc(T&& value) {

}

void main() {
	int a = 10;
	const int b = 100;
	MyFunc(a);		// T 为int&  发生引用折叠:int& && ---> int&
	MyFunc(b);		// T 为const int&   发生引用折叠:constt int& && ---> const int&
	MyFunc(100);	// T 为int,不发生引用折叠
	MyFunc(static_cast<const int&&>(100);	// T 为 const int,不发生引用折叠
}

实际上,代码中四次函数模板调用实例化的模板函数分别如下所示:

template<>
void MyFunc<int &>(int & value) {
}

template<>
void MyFunc<const int &>(const int & value) {
}

template<>
void MyFunc<int>(int && value) {
}

template<>
void MyFunc<const int>(const int && value) {
}

区分万能引用与右值引用

从形式上看,万能引用的语法格式与右值引用很相似, 都是类型名后面跟着 &&。

当 && 出现在代码中时,并不一定是右值引用:

void f(Widget&& param);             //右值引用
Widget&& var1 = Widget();           //右值引用
auto&& var2 = var1;                 //不是右值引用

template<typename T>
void func(std::vector<T>&& param);     //右值引用

template<typename T>
void func(T&& param);                  //不是右值引用

对一个万能引用而言,类型推导是必要的;除此之外,引用声明的形式必须正确,并且该形式是被限制的。

1)它必须严格匹配 “T&&” 形式 :

template <typename T>
void func(std::vector<T>&& param);     // param是一个右值引用

当函数func被调用的时候,类型T会被推导(除非调用者显式地指定它,这种边缘情况我们不考虑)。

但是param的类型声明并不是 T&&⚠️ ,而是std::vector<T>&&,是std::vector<T>&&,是std::vector<T>&&, 所以这里不是一个 万能引用,而是右值引用,因此只能绑定右值。 如果调用 函数 func 时传递了左值,编译器将会报错。

2)严格匹配 T&& 还要求不能出现多余的CV限定符,比如:

template <typename T>
void func(const T&& param);        // param是一个右值引用

这里也会导致 param 失去万能引用的资格,变成右值引用。

3)此外,对于一个类模板内部的成员函数来讲,相比普通的函数模板,又有一些特殊的情况。

一个模板里面的一个成员函数形参类型严格匹配 “T&&” 形式,未必是万能引用(通用引用), 因为类模板实例化的时候,其内部的成员函数未必发生类型推导。

template<class T, class Allocator = allocator<T>>   //来自C++标准
class vector
{
public:
    void push_back(T&& x);
    // ...
}

push_back函数的形参当然有一个通用引用的正确形式 T&&,然而,在这里并没有发生类型推导。因为push_back在有一个特定的vector实例之前不可能存在,而实例化vector时的类型已经决定了push_back的声明。也就是说,

std::vector<Widget> v;

将会导致std::vector模板被实例化为以下代码:

class vector<Widget, allocator<Widget>> {
public:
    void push_back(Widget&& x);             //右值引用};

可以清楚地看到,函数push_back不包含任何类型推导。

总结

  • 如果一个函数模板形参的类型为T&&,并且T需要被推导得知,或者如果一个对象被声明为auto&&,这个形参或者对象就是一个通用引用。
  • 如果类型声明的形式不是标准的type&&,或者如果类型推导没有发生,那么type&&代表一个右值引用。
  • 通用引用,如果它被右值初始化,就会对应地成为右值引用;如果它被左值初始化,就会成为左值引用。
<think>嗯,用户现在问的是如何区分万能引用引用。之前他们让我实现一个emplace函数,里面用到了完美转发和Args&&这样的参数。现在这个问题应该是在深入理解模板中的引用类型。 首先,我需要回顾一下Scott Meyers的术语,万能引用(universal reference)其实是他在书中提出的,后来标准委员会定名为转发引用(forwarding reference)。不过用户可能还是习惯叫万能引用。所以得先明确这两个术语的关系,避免混淆。 然后,区分的关键点在于类型推导发生的情况。当模板参数需要被推导时,T&&才可能是万能引用。例如,在函数模板中,参数是T&&,并且T是需要被推导的模板参数,这时候才是万能引用。而像void foo(int&& param)中的int&&,这里没有类型推导,直接就是引用。 可能用户会遇到类似这样的例子,比如模板函数中的参数和类成员函数中的参数的区别。例如,在模板函数中,参数是T&&,而在一个类中的成员函数,比如push_back,参数也是T&&,但这时候T是已知的类型,不需要推导,所以是引用。 另外,auto&&的情况也是万能引用,因为这里涉及类型推导。比如auto&& vec = some_vector;,vec的类型会根据some_vector的类别来决定,可能是左引用引用。 用户可能容易混淆的地方在于,看到T&&就以为是万能引用,但实际上只有在类型推导发生的情况下才是。比如在模板类的方法中,如果T已经确定,那么T&&就是引用。例如std::vector的push_back方法,它的参数是T&&,但此时T已经由vector的实例化类型确定,不需要推导,所以是引用。 需要举例说明这两种情况,让用户能够直观地看到区别。比如: 模板函数中的情况: template<typename T> void func(T&& param) {} // 这里是万能引用,因为T需要推导 而类成员函数的情况: template<class T> class Widget { public: void push_back(T&& x); // T已经由Widget实例化确定,无需推导,所以是引用 }; 此外,还要提到使用std::forward来处理万能引用,以实现完美转发,而引用通常用std::move来转移资源。 可能用户的实际问题是在编写模板代码时,如何正确使用引用类型,避免不必要的拷贝或者错误。因此,需要强调类型推导在区分这两种引用中的重要性,以及在不同上下文中的应用场景。 总结来说,区分的关键在于是否发生类型推导,以及模板参数是否处于被推导的上下文中。需要明确不同情况下的例子,帮助用户理解何时是万能引用,何时是引用。</think>在C++中区分万能引用(Universal Reference,C++17标准后改称转发引用)和引用(Rvalue Reference)的关键在于**类型推导的上下文**。以下是具体区分方法: --- ### **核心判断标准** | | 万能引用(转发引用) | 引用 | |------------------|------------------------------|-------------------------| | **语法形式** | `T&&` | `T&&` | | **触发条件** | 必须发生类型推导 | 类型已知不推导 | | **绑定能力** | 可绑定左、const/非const | 只能绑定 | | **典型场景** | 模板参数推导、`auto&&` | 非推导的明确类型声明 | --- ### **具体区分方法** #### 1. **模板函数中的类型推导** ```cpp template<typename T> void func(T&& param) {} // 万能引用(需要类型推导) ``` - 当调用`func(variable)`时: - 若`variable`是左,`T`推导为`T&`,最终类型为`T& &&` → 折叠为`T&` - 若`variable`是,`T`推导为`T`,最终类型为`T&&` #### 2. **明确类型的引用** ```cpp void func(int&& param) {} // 纯引用(类型已确定) void func(std::string&& s) {} // 纯引用 ``` - 只能接受参数,不接受左 #### 3. `auto&&`推导 ```cpp auto&& var = some_expression; // 万能引用(自动类型推导) ``` - `var`的类型根据侧表达式决定: - 若侧是左 → `auto&` - 若侧是 → `auto&&` #### 4. **类模板成员函数** ```cpp template<typename T> class Widget { public: void push_back(T&& x); // 引用(T已由类实例化确定) }; ``` - 此处`T`在类实例化时已确定,不涉及推导 --- ### **典型示例对比** #### 示例1:模板函数 vs 普通函数 ```cpp // 万能引用(转发引用) template<typename T> void universal_ref(T&& arg) { // 可接受任意类别 } // 引用 void rvalue_ref(int&& arg) { // 只能接受 } int main() { int x = 10; universal_ref(x); // ✔ 接受左 universal_ref(20); // ✔ 接受 rvalue_ref(x); // ❌ 编译错误 rvalue_ref(20); // ✔ 接受 } ``` #### 示例2:`auto&&`推导 ```cpp std::vector<int> create_vec() { return {1,2,3}; } int main() { std::vector<int> v1; auto&& v2 = v1; // v2类型:std::vector<int>& auto&& v3 = create_vec(); // v3类型:std::vector<int>&& } ``` --- ### **技术本质** - **万能引用**本质是**未决引用类型**(`deduced T&&`),依赖引用折叠规则: - `T& &` → `T&` - `T& &&` → `T&` - `T&& &` → `T&` - `T&& &&` → `T&&` - **引用**是**已确定的类型**,不参类型推导 --- ### **使用建议** - 需要完美转发时 → 使用万能引用(配合`std::forward`) - 需要明确移动语义时 → 使用引用(配合`std::move`)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值