Effective Modern C++ 条款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&&”)看起来像右值引用,但是它们可以表现左值引用(即“T&”)的行为。它们的双重性质允许它们绑定右值(就像右值引用那样)和左值(就像左值引用那样)。而且,它们可以绑定const或者非const对象,可以绑定volatile和非volatile对象,还可以绑定constvolatile同时作用的对象。它们实际上可以绑定任何东西。这种前所未有的灵活的引用应当有个名字,我称它们为通用引用(universal referense)。

通用引用在两种语境出现。最常见的就是模板函数参数,就如上面的代码中的最后一个例子:

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

第二个语境是auto声明,上面的代码中也含有这种情况:

auto&& var2 = var1;       // var2是通用引用

这两种语境有个共同点,就是出现了类型推断。在模板f中,param的类型需要被推断,而在var2的声明中,var2的类型要被推断。与接下来的例子对比(也是来自上面的代码),这些代码不用类型推断。如果你看见不带类型推断的“T&&”,那么你看见的是右值引用:

void f(Widget&& param);    // 没有类型推断,param是个右值引用

Widget&& var1 = Widget();   // 没有类型推断,var1是个右值引用

因为通用引用是引用,它们必须要被初始化。通用引用的初始值决定了它表现为左值引用还是右值引用。如果初始值是个左值,通用引用相当于左值引用,如果初始值是个右值,通用引用相当于右值引用。对于函数参数是通用引用的情况,调用端需要提供初始值:

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

Widget w;
f(w);       // 传递给f的是个左值,param的类型是Widget&(即是个左值引用)

f(std::move(w));  // 传递给f的是个右值,param的类型是Widget&&(即是个右值引用)

对于通用引用,类型推断是必需的,但还不足够。声明引用的形式一定要正确,声明的形式是强制的,它必须是精确的“T&&”。再看一次最开始的代码,里面有个这样的例子:

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

当使用f时,T的类型会被推断(除非调用者显式指定,这种边缘情况我们不关心),但是param的声明形式不是“T&&”,它是“std::vector&&”,param不符合通用引用的规则,所以它是个右值引用。如果你试图传递左值给f,编译器将会很开心地向你证明它是个右值引用:

std::vector<int> v;
f(v);           // 错误,右值引用不能绑定左值

就算是简单地出现const说明,也会取消一个引用成为通用引用的资格:

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

如果你在模板中看到个函数参数类型是“T&&”,你很可能就认定它是个通用引用了。很可惜你是错的,因为在模板中并不能保证会出现类型推断。思考std::vector的成员函数push_back

template<class T, class Allocator = alloctor<T>>  // 来自C++标准库
class vector {
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);    // 右值引用
    ...
};

现在你看到啦,push_back没有应用类型推断。std::vector的这个push_back(有两个push_back重载函数)总是把参数类型声明为rvalue-reference-to-T(对T的右值引用)。

相比之下,std::vector的概念相似的成员函数emplace_back使用了类型推断:

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

在这里,类型参数Args不是取决于vector的类型参数T,所以每次调用emplace_back都需要推断Args的类型。(好吧,Args实际上成是个参数包,不是类型参数,不过为了这里的讨论,我们可以把当他做是个类型参数)。

事实上,emplace_back的参数类型命名为Args,它依然个通用引用,巩固了我们之前说过的通用引用的正确形式是“T&&”,但我们没要求你使用类型参数名为T。例如,接下来的模板接受一个通用引用,因为形式(“type&&”)是正确的,而且param的类型会被推断(再次排除调用者显式指定类型的情况):

template<typename MyTemplateType>
void someFunc(MyTemplateType&& param)  // param是个通用引用

我在前面提到过auto变量也可以是通用引用,准确点说,声明为auto&&的变量都是通用引用,因为既发生了类型推断,又有正确的形式(T&&),auto通用引用没有模板函数参数那种通用引用场景,不过它们在C++11中偶尔会出现。它们在C++14中出现较多,因为C++14的lambda表达式可以声明auto&&参数。例如,如果你想要写一个C++14的lambda来记录调用任意函数执行的所需要的时间,你可以这样做:

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

如果你对“std::forward<decltype(blah blah blah)>”的反应是,“卧槽?”,那么很可能意味着你还没看条款33。不用担心,在本条款中,重要的东西是lambda表达式声明的auto&&参数。func是个通用引用 ,可以绑定任何的可执行对象,不论左值还是右值,而params是0个或多个通用引用(即通用引用参数包),可以绑定任何数目个任意类型对象 。最终结果是,感谢auto通用引用,让timeFuncInvocation可以记录几乎所有的函数执行的所需的时间。(关于“所有”和“几乎所有”的信息,请看条款30。)


请记住,这条款——通用引用的基础——是个谎言。。。额,应该说是“抽象”,埋藏在底下的真相是引用折叠(reference-collapsing),是条款28所讲的话题。不过真相不会让抽象的用处因此减少。区分右值引用和通用引用可以帮助你更准确地读代码(“我看到的T&&是只能绑定右值呢,还是可以绑定任何东西?”),然后当你和同事交流时,也可以避免说出含糊不清的话(“我这里用的是通用引用,不是右值引用。。。”)。这也让你能理解条款25和条款26,因为它们是取决于通用引用和右值引用的区别。所以,拥抱这抽象吧,陶醉于此。就像牛顿运动定律(实际上是不正确的),通常比使用爱因斯坦的相对论要容易和有用,因此,正常地使用通用引用的概念比研究引用折叠的细节更可取。


总结

需要记住的3点:

  • 如果一个模板函数的参数类型是T&&而T是要被推断类型,或者一个对象使用auto&&声明,那么这个参数或者对象是个通用引用。
  • 如果类型推断的类型不是精确的type&&,或者不发生类型推断,那么typ&&指代的是右值引用。
  • 如果用右值初始化通用引用,通用引用相当于右值引用;如果用左值初始化通用引用,通用引用相当于左值引用。
Coming to grips with C++11 and C++14 is more than a matter of familiarizing yourself with the features they introduce (e.g., auto type declarations, move semantics, lambda expressions, and concurrency support). The challenge is learning to use those features effectively—so that your software is correct, efficient, maintainable, and portable. That’s where this practical book comes in. It describes how to write truly great software using C++11 and C++14—i.e. using modern C++. Topics include: The pros and cons of braced initialization, noexcept specifications, perfect forwarding, and smart pointer make functions The relationships among std::move, std::forward, rvalue references, and universal references Techniques for writing clear, correct, effective lambda expressions How std::atomic differs from volatile, how each should be used, and how they relate to C++'s concurrency API How best practices in "old" C++ programming (i.e., C++98) require revision for software development in modern C++ Effective Modern C++ follows the proven guideline-based, example-driven format of Scott Meyers' earlier books, but covers entirely new material. "After I learned the C++ basics, I then learned how to use C++ in production code from Meyer's series of Effective C++ books. Effective Modern C++ is the most important how-to book for advice on key guidelines, styles, and idioms to use modern C++ effectively and well. Don't own it yet? Buy this one. Now". -- Herb Sutter, Chair of ISO C++ Standards Committee and C++ Software Architect at Microsoft
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值