条款24:区别开统一引用和右值引用

        经常说,真相会使人自由,但在某些环境下,一个精心挑选的谎言也同样使人释放。本条款就是这样一个谎言。但因为我们面对的是软件,我们还是不谈“谎言”,我们说本条款包含了一个“抽象”。

        为了声明一个指向某类型T的右值引用,你会写成T&&。于是很合理的设想,你在代码中看到“T&&”,你会把它当作一个右值引用。但其实并没有这么简单:

void f(Widget&& param);      // 右值引用  

Widget&& var1 = Widget();    // 右值引用

auto&& var2 = var1;          // 不是右值引用

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

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


        实际上,“T&&”有两个不同的含义。一个当然是右值引用,这个引用表现出你所期望的:它们仅仅绑定到右值,它们的主要差事就是识别出那些可以被移动的对象。

        “T&&”另一个含义是既是右值引用,又是左值引用。这样的引用在代码中看上去像右值引用(也就是T&&),但它们可以表现的像是左值引用。它们的双重特性使之可以既绑定到右值(像右值引用一样),也可以绑定到左值(像左值引用)。此外它们也可以绑定到const或非const对象,以及volatile或非volatile对象,甚至绑定到那些既是const也是volatile的对象。它们可以虚拟的绑定到任何东西上。这史无前例的灵活性需要一个它们自己的名字,我管它们称为统一引用

        统一引用出现在两种场景下。最常见的是函数模板的参数,比如下面这个例子(来自上面的示例代码):

template<typename T>
void f(T&& param);  // param 是一个统一引用

        第二个场景是auto声明,统一来自上面示例代码

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

        这些场景共同之处是有类型推导的出现。在模板f中,param的类型是推导的,对var2的声明中,var2的类型也是推导的。相比下面的代码(也是来自上面的示例代码),是没有类型推导的。假如你看到”T&&“没有类型推导,它就是一个右值引用。

void f(Widget&& param);   // no type deduction;
                          // param is an rvalue reference
Widget&& var1 = Widget();  // no type deduction;
                           // var1 is an rvalue reference

        因为统一引用是引用,所以必须初始化。统一引用的初始化者决定了它代表左值还是右值引用。初始化者如果是右值,则统一引用关联到一个右值引用。如果初始化者是个左值,则统一引用关联到一个左值引用。对于函数参数的统一引用,初始化者如下提供:

template<typename T>
void f(T&& param);   // param is a universal reference

Widget w;
f(w);                // lvalue passed to f; param's type is
                     // Widget& (i.e., an lvalue reference)
f(std::move(w));     // rvalue passed to f; param's type is
                     // Widget&& (i.e., an rvalue reference)

        对统一引用来说,类型推导是必须的,但是还不够。引用声明的形式必须正确,形式很受限制,必须是“T&&”。再看看早前的示例代码:

template<typename T>
void f(std::vector<T>&& param); // 参数是右值引用

当f被调用,T类型会进行推导(除非调用者显式的指明它,边界情况我们不关注),但param的类型声明不是“T&&”,是“std::vector<T>&&”。这排除了param是一个统一引用的可能性,因此param是个右值引用。假如你打算传递一个左值给f,编译器会很乐意给你报错:

std::vector<int> v;
f(v);                         // error! can't bind lvalue to
                              // rvalue reference

        即使是最简单的const符也足够使得引用不再是一个统一引用:

template<typname T>

void f(const T&& param) //param is an rvalue reference

        假如在一个模板中,你看到一个函数参数是类型T&&,你可能会认为它一定是统一引用。不一定。因为在模板中不能断定一定有类型推导。考虑下面这个在std::vector中的push_back成员函数:

template<class T, class Allocator = allocator<T>>   // from C++
class vector {                                      // Standards
public:
    void push_back(T&& x);
    …
};

        push_back的参数的确有统一引用的正确形式,但在这个场景下没有类型推导。因为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不包含类型推导。这个vector<T>的push_back函数(有两个这样的重载函数)总是声明了参数为指向T类型的右值引用。

        相反,vector里概念上相似的成员函数emplace_back包含了类型推导:

template<class T, class Allocator = allocator<T>>         // still from
class vector {                                            // C++
public:                                                   // Standards
    template <class... Args>
    void emplace_back(Args&&... args);
    …
};

        这里,类型参数Args是独立于vector的类型参数T的,所以,Args必须在每次emplace_back被调用时进行推导(当然,Args实际上是个参数包(pack),不是真正的类型参数,但出于这里讨论的目的,我们可以假设它是个类型参数)。

        emplace_back的类型参数是Args,仍然是一个统一引用,这个事实强化了我之前说的统一引用的形式是“T&&”,但没必要一定要用T。比如下面的模板用了统一引用,因为形式(type&&)是对的,param的类型会被推导(除了那些调用者明确指定了类型的):

template<typename MyTemplateType>          // param is a
void someFunc(MyTemplateType&& param);     // universal reference


        我之前也说过auto变量也可以作为统一引用。更准确的说是声明类型为auto&&的变量是统一引用,因为发生了类型推导并且它们有正确的形式(“T&&”)。auto统一引用用在函数模板参数中没有统一引用多,但它们的确时不时的出现在c++11中。在C++14中,它们出现的更多,因为c++14的lamda表达式可以声明auto&&参数。比如,你想写一个c++14的lamda表达式来记录任意函数调用中的时间消耗,你可以这样:

auto timeFuncInvocation =
[](auto&& func, auto&&... params)                      // C++14
{
    start timer;
    std::forward<decltype(func)>(func)(                // invoke func
    std::forward<decltype(params)>(params)...          // on params
    );
    stop timer and record elapsed time;
};

        假如你对“std::forward<decltype(blah blah blah)>”代码很困惑,很可能是你没有看过条款33,没关系,在本条款里,这里的重点是这个lamda声明的auto&&参数。func是一个统一引用,可以被绑定到任何可调用对象,左值或右值。params是0个或更多的统一引用(也就是一个统一引用参数包),可以被绑定任何数量的任意类型的对象。多亏了auto统一引用,结果是timeFuncInvocation可以计算出大多数任何函数执行时间。(关于“任何”和“大多数任何”的区别,可以见条款30).

        整个本条款需要记住的是,统一引用的概念是个谎...哦是个“抽象”。隐藏的事实是引用倒塌,条款28会描述。但事实并没有使得这个抽象无用。区分开右值引用和统一引用会帮助你更精确的阅读代码(“我看到的T&&是只绑定了右值还是任何东西?”),它也可以避免你和同事交流中的似是而非(“我这里用的是统一引用,而不是右值引用...”)。也会使你对条款25和26明白,它们依赖了这个区别。所以拥抱这个抽象吧,在其中狂欢吧。就像牛顿定律(科学上不正确的)比爱因斯坦的相对论(“真理”)更典型的有用,更容易被接受一样,统一引用也比引用倒塌的细节更容易使用。

                                                  

 应该记住的事情

1.假如一个函数模板的参数有一个T&&类型用来推导,或者对象使用auto&&来声明,这个参数或对象是一个统一引用。

2.假如类型声明的形式不是精确的type&&,或者没有发生类型推导,type&&表示了一个右值引用。

3.假如统一引用被右值来初始化,则它们关联到右值。如果被左值来初始化,则它们关联到左值。


  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
转发引用(forwarding reference)和引用(rvalue reference)是C++中两个相关但不完全相同的概念。 转发引用(forwarding reference)是C++11引入的一种特殊的引用类型,使用模板类型推导结合引用语法(&&)表示。它的主要特点是它的类型是根据实参的类别(左还是)来推导得出的。当使用转发引用声明函数模板参数时,可以接受左,并将其原始类型和类别保持不变。转发引用常常与完美转发(perfect forwarding)一起使用,用于在函数模板中传递参数。 引用(rvalue reference)是C++11引入的另一种引用类型,使用&&表示,主要用于绑定到临时对象、表达式和可以被移动的对象。引用允许对进行特殊操作,例如移动语义和完美转发。通过引用,可以将资源所有权从一个对象转移到另一个对象,提高代码的效率。 区别: - 转发引用是一种特殊的引用类型,在函数模板中使用,根据实参的类别来推导其类型。而引用则是一种普通的引用类型,用于绑定到临时对象、表达式和可以被移动的对象。 - 转发引用常常用于实现完美转发,保持原始参数的类型和类别。而引用主要用于实现移动语义和转移资源所有权。 - 转发引用在函数模板中使用,可以接受左引用可以绑定到,但不能绑定到左。 - 转发引用使用模板类型推导来推导其类型,可以根据实参的类别来确定是左引用还是引用引用是通过使用&&来声明的,表示特定的引用类型。 需要注意的是,转发引用引用可以在某些情况下产生相似的效果,但它们的语义和使用场景有所不同。 希望能解答你的问题!如果还有其他问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值