Effective Modern C++ 条款24 区分通用引用和右值引用

区分通用引用和右值引用

有人说,真相可以给你自由,但在某些情况下,一个适当的谎言同样可以解放你。本条款就是这样的谎言,但是,因为我们处理的是软件问题,所以我们回避“谎言”这个词,取而代之,我们说本条款由“抽象”组成。

对类型T声明一个右值引用,你可以写成T&&,因此每当你在源代码中看到“T&&”时,都以为那是个右值引用。哎,没那么简单:

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

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

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

template<typename T>
void f(std::vector&& 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&& 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>  // 来自C++标准库
class vector {
public:
    void push_back(T&& x);
    ...
};

push_back的参数类型是通用引用的正确形式,但在这个例子中没有类型推断。那是因为push_back只能是一个实例化的vector的一部分,然后实例化的类型已经决定push_back的参数类型了。也就是说:

std::vector v;

会导致std::vector模板实例化成这样:

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


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


总结

需要记住的3点:

  • 如果一个模板函数的参数类型是T&&而T是要被推断类型,或者一个对象使用auto&&声明,那么这个参数或者对象是个通用引用。
  • 如果类型推断的类型不是精确的type&&,或者不发生类型推断,那么typ&&指代的是右值引用。
  • 如果用右值初始化通用引用,通用引用相当于右值引用;如果用左值初始化通用引用,通用引用相当于左值引用。

你可能感兴趣的:(Effective,Modern,C++,Effective,Modern,C++,c++)