Item 24: Distinguish universal references from rvalue references

为了声明指向某类型T的右值引用,你会写T&&。这会使我们在看到源码种出现“T&&”时,就认为是一个右值引用。但,没那么简单:

void f(Widget&& param);             // rvalue reference

Widget&& var1 = Widget();           // rvalue reference

auto&& var2 = var1;                 // not rvalue reference

template<typename T>
void f(std::vector<T>&& param);     // rvalue reference

template<typename T>
void f(T&& param);                  // not rvalue reference

事实上,“T&&” 有两种不同的涵义。当然,其中一个是右值引用的意思。这种引用行为就是你所期望的:它们只绑定到右值上去,并且它们的主要职责就是去明确一个对象是可以被move的。
“T&&” 的另一种含义是,则表示其既可以是右值引用,也可以是左值引用。带有这种含义的引用在代码中形如右值引用(即 “T&&”),但它们能表现的像左值引用一样(即 “T&”)。这种双重特性使之既可以绑定到右值(如右值引用),也可以绑定到左值(左值引用)。此外,它们也可以绑定到const对象或非const对象,以及volatile对象或非volatile对象,甚至绑定到既带有const又带有volatile属性的对象。它们几乎可以绑定到任何东西。我们称这种史无前例的灵活性的引用为万能引用【Item 25将会解释万能应用几乎总是需要应用std::forward。目前C++ 委员会的一些成员开始称其为转发引用(forwarding references)】。

万能引用出现在两种场景下。最常用的场景是函数模板参数,比如上面的代码:

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

另外一种场景是auto声明,比如上面代码中的:

auto&& var2 = var1;             // var2 is a universal reference

这两种场景的共同之处,在于它们都涉及到类型推导。在模板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

因为万能引用也是引用,所以它必须被初始化。万能应用的初始化物(initializer)决定了它代表的是左值引用还是右值引用。如果initializer是右值,万能引用相当于右值引用,如果initializer是左值,则万能应用相当于左值引用。对于作为函数形参的万能引用而言,初始化物(initializer)在调用出提供:

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&&” 才行【换一种好理解的表述:如果函数模板形参具备T&&格式,且T类型是推导而来,或者对象使用auto&&声明其类型,则该形参或对象就是个万能引用】。再看一次这个我们之前在示例代码中看过的例子:

template<typename T>
void f(std::vector<T>&& param);  // param is an rvalue reference

当f被调用时,类型T将被推导(除非调用者显示指明了类型,这是一种我们不必关心的边界情况),但paramr的类型声明形式不是 “T&&”,而是 “std::vector&&”。这就排除了param是万能引用的可能性。因此,param是一个右值引用,如果你尝试向f传递左值,编译器会很乐意为你确认出来:

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

即使是一个const修饰的存在,也足以褫夺一个引用成为万能引用的资格:

template<typename T>
void f(const T&& param);    // param is an rvalue reference

如果在模板内看到一个函数的形参类型写作 “T&&”,你可能想当然的认为它肯定是一个哇能引用。其实不然。因为位于模板内并不保证一定涉及类型推导,考虑如下代码:

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

push_back的形参格式虽然符合万能引用的格式,但是并不涉及类型推导。因为push_back不能存在于vector的特定实例之外,并且实例的类型就完全能决定push_back的声明类型了。也就是说:

std::vector<Widget> v;

使得std::vector模板被实例化为下面这样:

class vector<Widget, allocator<Widget>> {
public:
    void push_back(Widget&& x);             // rvalue reference};

现在你能清楚地发现push_back没有用到类型推导。所以这个push_back是指向T的右值引用。

std::vector中还有一个与push_back概念类似的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,所以每次emplace_back被调用的时候,Args必须被推导。(好吧,Args事实上是一个参数包,不是一个类型参数,但是为了讨论的目的,我们能把它视为一个类型参数。)

前面提到过aoto变量也可以作为万能引用。准确地说,声明为auto&&类型的变量都是万能引用,因为它们既涉及到类型推导,也有正确的格式(“T&&”)。 auto万能引用在C++ 11中没有像函数模板形参的万能引用那么常见,但是在C++ 14中却经常出现,尤其是写一个lambda表达式:

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)>”这种形式的代码迷惑,不过没关系,Item 33中会有详细展示。

其实呢,本条款所说的万能引用是一个抽象的说法,它底层的真相被称为“引用折叠”。Item 28会专门讲这个问题。之所以本条款讨论了万能引用和右值引用的区别,目的是为了我们能更精准的阅读代码(“我看到的T&&只能绑定到右值上,还是能绑定到所有东西上呢?”),并且在你和同事讨论的时候,它能让你避免歧义。(“我在这里使用一个universal引用,不是一个右值引用…”)。它也能让你搞懂Item 25和Item 26的意思,这两个Item都依赖于这两个引用的区别。另外,掌握万能引用的概念会比了解引用折叠的技术细节是个更好的选择。

Things to Remember

  • 如果函数模板形参具备T&&格式,并且T类型是推导而来,或者对象使用auto&&声明其类型,则该形参或对象就是个万能引用;
  • 如果类型声明并不精确的匹配type&&格式,类型推导没有发生,type&&就代表右值引用;
  • 如果采用右值初始化万能引用,就会得到一个右值引用;如果用左值初始化万能引用,就会得到一个左值引用;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值