Universal References in C++11—Scott Meyers

来源:互联网 发布:淘宝英伦男装店铺排行 编辑:程序博客网 时间:2024/05/17 06:13

T&& Doesn’t Always Mean “Rvalue Reference”

by Scott Meyers

Related materials:

  • A video of Scott’s C&B talk based on this material is available on Channel 9.
  • A black-and-white PDF version of this article is available in Overload 111.

 

Perhaps the most significant new feature in C++11 is rvalue references; they’re the foundation on which move semantics and perfect forwarding are built. (If you’re unfamiliar with the basics of rvalue references, move semantics, or perfect forwarding, you may wish to read Thomas Becker’s overview before continuing.)

Syntactically, rvalue references are declared like “normal” references (now known aslvalue references), except you use two ampersands instead of one.  This function takes a parameter of type rvalue-reference-to-Widget:

  1. void f(Widget&& param);

Given that rvalue references are declared using “&&”, it seems reasonable to assume that the presence of “&&” in a type declaration indicates an rvalue reference. That is not the case:

  1. Widget&& var1 = someWidget; // here, “&&” means rvalue reference
  2.  
  3. auto&& var2 = var1; // here, “&&” does not mean rvalue reference
  4.  
  5. template<typename T>
  6. void f(std::vector<T>&& param); // here, “&&” means rvalue reference
  7.  
  8. template<typename T>
  9. void f(T&& param);               // here, “&&”does not mean rvalue reference

In this article, I describe the two meanings of “&&” in type declarations, explain how to tell them apart, and introduce new terminology that makes it possible to unambiguously communicate which meaning of “&&” is intended. Distinguishing the different meanings is important, because if you think “rvalue reference” whenever you see “&&” in a type declaration, you’ll misread a lot of C++11 code.

The essence of the issue is that “&&” in a type declaration sometimes means rvalue reference, but sometimes it meanseither rvalue referenceor lvalue reference. As such, some occurrences of “&&” in source code may actually have the meaning of “&”, i.e., have the syntacticappearance of an rvalue reference (“&&”), but themeaning of an lvalue reference (“&”).  References where this is possible are more flexible than either lvalue references or rvalue references.Rvalue references may bind only torvalues, for example, and lvalue references, in addition to being able to bind to lvalues, may bind to rvalues only under restricted circumstances.[1] In contrast, references declared with “&&” that may be either lvalue references or rvalue references may bind toanythingSuch unusually flexible references deserve their own name. I call themuniversal references.

The details of when “&&” indicates a universal reference (i.e., when “&&” in source code might actually mean “&”) are tricky, so I’m going to postpone coverage of the minutiae until later. For now, let’s focus on the following rule of thumb, because that is what you need to remember during day-to-day programming:

If a variable or parameter is declared to have typeT&& for somededuced type T, that variable or parameter is auniversal reference.

The requirement that type deduction be involved limits the situations where universal references can be found.In practice, almost all universal references are parameters to function templates.Because the type deduction rules for auto-declared variables are essentially the same as for templates, it’s also possible to haveauto-declared universal references. These are uncommon in production code, but I show some in this article, because they are less verbose in examples than templates.  In theNitty Gritty Details section of this article, I explain that it’s also possible for universal references to arise in conjunction with uses oftypedef anddecltype, but until we get down to the nitty gritty details, I’m going to proceed as if universal references pertained only to function template parameters andauto-declared variables.

The constraint that the form of a universal reference be T&& is more significant than it may appear, but I’ll defer examination of that until a bit later. For now, please simply make a mental note of the requirement.

Like all references, universal references must be initialized, and it is a universal reference’s initializer that determines whether it represents an lvalue reference or an rvalue reference:

  • If the expression initializing a universal reference is an lvalue, the universal reference becomes an lvalue reference.
  • If the expression initializing the universal reference is an rvalue, the universal reference becomes an rvalue reference.

This information is useful only if you are able to distinguish lvalues from rvalues.  A precise definition for these terms is difficult to develop (the C++11 standard generally specifies whether an expression is an lvalue or an rvalue on a case-by-case basis), but in practice, the following suffices:

  • If you can take the address of an expression, the expression is an lvalue.
  • If the type of an expression is an lvalue reference (e.g.,T& orconst T&, etc.), that expression is an lvalue. 
  • Otherwise, the expression is an rvalue.  Conceptually (and typically also in fact), rvalues correspond to temporary objects, such as those returned from functions or created through implicit type conversions. Most literal values (e.g., 10 and 5.3) are also rvalues.

Consider again the following code from the beginning of this article:

  1. Widget&& var1 = someWidget;
  2. auto&& var2 = var1;

You can take the address of var1, so var1 is an lvalue. var2’s type declaration ofauto&& makes it a universal reference, and because it’s being initialized withvar1 (an lvalue),var2 becomes an lvalue reference.  A casual reading of the source code could lead you to believe thatvar2 was an rvalue reference; the “&&” in its declaration certainly suggests that conclusion.  But because it is a universal reference being initialized with an lvalue,var2 becomes an lvalue reference.  It’s as ifvar2 were declared like this:

  1. Widget& var2 = var1;

As noted above, if an expression has type lvalue reference, it’s an lvalue.  Consider this example:

  1. std::vector<int> v;
  2. ...
  3. auto&& val = v[0]; // val becomes an lvalue reference (see below)

val is a universal reference, and it’s being initialized with v[0], i.e., with the result of a call to std::vector<int>::operator[]. That function returns an lvalue reference to an element of the vector.[2]   Because all lvalue references are lvalues, and because this lvalue is used to initializeval,val becomes an lvalue reference, even though it’s declared with what looks like an rvalue reference.

I remarked that universal references are most common as parameters in template functions.  Consider again this template from the beginning of this article:

  1. template<typename T>
  2. void f(T&& param); // “&&” might mean rvalue reference

Given this call to f,

  1. f(10); // 10 is an rvalue

param is initialized with the literal 10, which, because you can’t take its address, is an rvalue.  That means that in the call tof, the universal referenceparam is initialized with an rvalue, soparam becomes an rvalue reference – in particular,int&&.

On the other hand, if f is called like this,

  1. int x = 10;
  2. f(x); // x is an lvalue

param is initialized with the variable x, which, because you can take its address, is an lvalue.  That means that in this call tof, the universal reference param is initialized with an lvalue, and param therefore becomes an lvalue reference –int&, to be precise.

The comment next to the declaration of f should now be clear:  whetherparam’s type is an lvalue reference or an rvalue reference depends on what is passed whenf is called.  Sometimes param becomes an lvalue reference, and sometimes it becomes an rvalue reference. param really is a universal reference.

Remember that “&&” indicates a universal referenceonly where type deduction takes place. Where there’s no type deduction, there’s no universal reference. In such cases, “&&” in type declarations always means rvalue reference.  Hence:

  1. template<typename T>
  2. void f(T&& param); // deduced parameter type ⇒ type deduction;
  3. // && ≡ universal reference
  4.  
  5. template<typename T>
  6. class Widget {
  7. ...
  8. Widget(Widget&& rhs); // fully specified parameter type ⇒ no type deduction;
  9. ... // && ≡ rvalue reference
  10. };
  11.  
  12. template<typename T1>
  13. class Gadget {
  14. ...
  15. template<typename T2>
  16. Gadget(T2&& rhs); // deduced parameter type ⇒ type deduction;
  17. ... // && ≡ universal reference
  18. };
  19.  
  20. void f(Widget&& param); // fully specified parameter type ⇒ no type deduction;
  21. // && ≡ rvalue reference

There’s nothing surprising about these examples.  In each case, if you see T&& (where T is a template parameter), there’s type deduction, so you’re looking at a universal reference.  And if you see “&&” after a particular type name (e.g.,Widget&&), you’re looking at an rvalue reference.

I stated that the form of the reference declaration must beT&& in order for the reference to be universal. That’s an important caveat.  Look again at this declaration from the beginning of this article:

  1. template<typename T>
  2. void f(std::vector<T>&& param); // “&&” means rvalue reference

Here, we have both type deduction and a “&&”-declared function parameter, but the form of the parameter declaration is not “T&&”, it’s “std::vector<t>&&”.  As a result, the parameter is a normal rvalue reference, not a universal reference.  Universal references can only occur in the form “T&&”!  Even the simple addition of aconst qualifieris enough to disable the interpretation of “&&” as a universal reference:

  1. template<typename T>
  2. void f(const T&& param); // “&&” means rvalue reference

Now, “T&&” is simply the required form for a universal reference.  It doesn’t mean you have to use the nameT for your template parameter:

  1. template<typename MyTemplateParamType>
  2. void f(MyTemplateParamType&& param); // “&&” means universal reference

Sometimes you can see T&& in a function template declaration whereT is a template parameter, yet there’s still no type deduction.  Consider thispush_back function instd::vector:[3]

  1. template <class T, class Allocator = allocator<T> >
  2. class vector {
  3. public:
  4. ...
  5. void push_back(T&& x); // fully specified parameter type ⇒ no type deduction;
  6. ... // && ≡ rvalue reference
  7. };

Here, T is a template parameter, and push_back takes aT&&, yet the parameter is not a universal reference!  How can that be?

The answer becomes apparent if we look at how push_back would be declared outside the class. I’m going to pretend thatstd::vector’sAllocator parameter doesn’t exist, because it’s irrelevant to the discussion, and it just clutters up the code.  With that in mind, here’s the declaration for this version ofstd::vector::push_back:

  1. template <class T>
  2. void vector<T>::push_back(T&& x);

push_back can’t exist without the class std::vector<T> that contains it.  But if we have a classstd::vector<T>, we already know whatT is, so there’s no need to deduce it.

An example will help.  If I write

  1. Widget makeWidget(); // factory function for Widget
  2. std::vector<Widget> vw;
  3. ...
  4. Widget w;
  5. vw.push_back(makeWidget()); // create Widget from factory, add it to vw

my use of push_back will cause the compiler to instantiate that function for the classstd::vector<Widget>. The declaration for thatpush_back looks like this:

  1. void std::vector<Widget>::push_back(Widget&& x);

See?  Once we know that the class is std::vector<Widget>, the type ofpush_back’s parameter is fully determined:  it’sWidget&&.  There’s no role here for type deduction.

Contrast that with std::vector’s emplace_back, which is declared like this:

  1. template <class T, class Allocator = allocator<T> >
  2. class vector {
  3. public:
  4. ...
  5. template <class... Args>
  6. void emplace_back(Args&&... args); // deduced parameter types ⇒ type deduction;
  7. ... // && ≡ universal references
  8. };

Don’t let the fact that emplace_back takes a variable number of arguments (as indicated by the ellipses in the declarations forArgs andargs) distract you from the fact that a type for each of those arguments must be deduced.  The function template parameterArgs is independent of the class template parameterT, so even if we know that the class is, say,std::vector<Widget>, that doesn’t tell us the type(s) taken byemplace_back.  The out-of-class declaration for emplace_back forstd::vector<Widget> makes that clear (I’m continuing to ignore the existence of theAllocator parameter):

  1. template<class... Args>
  2. void std::vector<Widget>::emplace_back(Args&&... args);

Clearly, knowing that the class is std::vector<Widget> doesn’t eliminate the need for the compiler to deduce the type(s) passed toemplace_back.  As a result,std::vector::emplace_back’s parameters are universal references, unlike the parameter to the version ofstd::vector::push_back we examined, which is an rvalue reference.

A final point is worth bearing in mind: the lvalueness or rvalueness of an expression is independent of its type. Consider the typeint.  There are lvalues of typeint (e.g., variables declared to beints), and there are rvalues of typeint (e.g., literals like10).  It’s the same for user-defined types likeWidget. AWidget object can be an lvalue (e.g., a Widget variable) or an rvalue (e.g., an object returned from aWidget-creating factory function). The type of an expression does not tell you whether it is an lvalue or an rvalue.
Because the lvalueness or rvalueness of an expression is independent of its type, it’s possible to havelvalues whose type isrvalue reference, and it’s also possible to havervalues of the typervalue reference:

  1. Widget makeWidget(); // factory function for Widget
  2.  
  3. Widget&& var1 = makeWidget() // var1 is an lvalue, but
  4. // its type is rvalue reference (to Widget)
  5.  
  6. Widget var2 = static_cast<Widget&&>(var1); // the cast expression yields an rvalue, but
  7. // its type is rvalue reference (to Widget)

The conventional way to turn lvalues (such asvar1) into rvalues is to usestd::move on them, sovar2 could be defined like this:

  1. Widget var2 = std::move(var1); // equivalent to above

I initially showed the code with static_cast only to make explicit that the type of the expression was an rvalue reference (Widget&&).

Named variables and parameters of rvalue reference type are lvalues. (You can take their addresses.)Consider again theWidget and Gadget templates from earlier:

  1. template<typename T>
  2. class Widget {
  3. ...
  4. Widget(Widget&& rhs); // rhs’s type is rvalue reference,
  5. ... // but rhs itself is an lvalue
  6. };
  7.  
  8. template<typename T1>
  9. class Gadget {
  10. ...
  11. template <typename T2>
  12. Gadget(T2&& rhs); // rhs is a universal reference whose type will
  13. ... // eventually become an rvalue reference or
  14. }; // an lvalue reference, but rhs itself is an lvalue

In Widget’s constructor,rhs is an rvalue reference, so we know it’s bound to an rvalue (i.e., an rvalue was passed to it), butrhs itself is an lvalue, so we have to convert it back to an rvalue if we want to take advantage of the rvalueness of what it’s bound to.  Our motivation for this is generally to use it as the source of a move operation, and that’s why the way to convert an lvalue to an rvalue is to usestd::move.  Similarly, rhs inGadget’s constructor is a universal reference, so it might be bound to an lvalue or to an rvalue, but regardless of what it’s bound to,rhs itself is an lvalue.  If it’s bound to an rvalue and we want to take advantage of the rvalueness of what it’s bound to, we have to convertrhs back into an rvalue. If it’s bound to an lvalue, of course, we don’t want to treat it like an rvalue.  This ambiguity regarding the lvalueness and rvalueness of what a universal reference is bound to is the motivation forstd::forward:  to take a universal reference lvalue and convert it into an rvalue only if the expression it’s bound to is an rvalue.  The name of the function (“forward”) is an acknowledgment that our desire to perform such a conversion is virtually always to preserve the calling argument’s lvalueness or rvalueness when passing –forwarding – it to another function.

But std::move and std::forward are not the focus of this article.  The fact that “&&” in type declarations may or may not declare an rvalue reference is.  To avoid diluting that focus, I’ll refer you to the references in the Further Information section for information on std::move and std::forward.

Nitty Gritty Details

The true core of the issue is that some constructs in C++11 give rise to references to references, and references to references are not permitted in C++. If source code explicitly contains a reference to a reference, the code is invalid:

  1. Widget w1;
  2. ...
  3. Widget& & w2 = w1; // error! No such thing as “reference to reference”

There are cases, however, where references to references arise as a result of type manipulations that take place during compilation, and in such cases, rejecting the code would be problematic. We know this from experience with the initial standard for C++, i.e., C++98/C++03.

During type deduction for a template parameter that is a universal reference, lvalues and rvalues of the same type are deduced to have slightly different types.  In particular, lvalues of typeT are deduced to be of type T& (i.e., lvalue reference toT), while rvalues of typeT are deduced to be simply of typeT. (Note that while lvalues are deduced to be lvalue references, rvalues are not deduced to be rvalue references!)Consider what happens when a template function taking a universal reference is invoked with an rvalue and with an lvalue:

  1. template<typename T>
  2. void f(T&& param);
  3.  
  4. ...
  5.  
  6. int x;
  7.  
  8. ...
  9.  
  10. f(10); // invoke f on rvalue
  11. f(x); // invoke f on lvalue

In the call to f with the rvalue 10, T is deduced to beint, and the instantiatedf looks like this:

  1. void f(int&& param); // f instantiated from rvalue

That’s fine. In the call to f with the lvalue x, however,T is deduced to beint&, and f’s instantiation contains a reference to a reference:

  1. void f(int& && param); // initial instantiation of f with lvalue

Because of the reference-to-reference, this instantiated code is prima facie invalid, but the source code– “f(x)” – is completely reasonable. To avoid rejecting it, C++11 performs“reference collapsing” when references to references arise in contexts such as template instantiation.

Because there are two kinds of references (lvalue references and rvalue references), there are four possible reference-reference combinations: lvalue reference to lvalue reference, lvalue reference to rvalue reference, rvalue reference to lvalue reference, and rvalue reference to rvalue reference.  There are only two reference-collapsing rules:

  • An rvalue reference to an rvalue reference becomes (“collapses into”) an rvalue reference.
  • All other references to references (i.e., all combinations involving an lvalue reference) collapse into an lvalue reference.

Applying these rules to the instantiation of f on an lvalue yields the following valid code, which is how the compiler treats the call:

  1. void f(int& param); // instantiation of f with lvalue after reference collapsing

This demonstrates the precise mechanism by which a universal reference can, after type deduction and reference collapsing, become an lvalue reference.The truth is that a universal reference is really just anrvalue referencein a reference-collapsing context.

Things get subtler when deducing the type for a variable that is itself a reference. In that case, the reference part of the type is ignored.  For example, given

  1. int x;
  2.  
  3. ...
  4.  
  5. int&& r1 = 10; // r1’s type is int&&
  6.  
  7. int& r2 = x; // r2’s type is int&

the type for both r1 andr2 is considered to beintin a call to the templatef.  This reference-strippingbehavior isindependent of the rule that, during type deduction for universal references,lvalues are deduced to be of typeT& and rvalues of type T, so given these calls,

  1. f(r1);
  2.  
  3. f(r2);

the deduced type for both r1 and r2 is int&. Why? First the reference parts ofr1’s andr2’s types are stripped off (yielding int in both cases), then, because each is an lvalue, each is treated asint& during type deduction for the universal reference parameter in the call tof.

Reference collapsing occurs, as I’ve noted, in “contexts such as template instantiation.” A second such context is the definition ofauto variables.  Type deduction forauto variables that are universal references is essentially identical to type deduction for function template parameters that are universal references, so lvalues of typeT are deduced to have typeT&, and rvalues of type T are deduced to have type T Consider again this example from the beginning of this article:

  1. Widget&& var1 = someWidget; // var1 is of type Widget&& (no use of auto here)
  2.  
  3. auto&& var2 = var1; // var2 is of type Widget& (see below)

var1 is of type Widget&&, but its reference-ness is ignored during type deduction in the initialization ofvar2; it’s considered to be of typeWidget.  Because it’s an lvalue being used to initialize a universal reference (var2), its deduced type isWidget&.  SubstitutingWidget& for auto in the definition for var2 yields the following invalid code,

  1. Widget& && var2 = var1; // note reference-to-reference

which, after reference collapsing, becomes

  1. Widget& var2 = var1; // var2 is of type Widget&

A third reference-collapsing context istypedef formation and use. Given this class template,

  1. template<typename T>
  2. class Widget {
  3. typedef T& LvalueRefType;
  4. ...
  5. };

and this use of the template,

  1. Widget<int&> w;

the instantiated class would contain this (invalid) typedef:

  1. typedef int& & LvalueRefType;

Reference-collapsing reduces it to this legitimate code:

  1. typedef int& LvalueRefType;

If we then use this typedef in a context where references are applied to it, e.g.,

  1. void f(Widget<int&>::LvalueRefType&& param);

the following invalid code is produced after expansion of the typedef,

  1. void f(int& && param);

but reference-collapsing kicks in, sof’s ultimate declaration is this:

  1. void f(int& param);

The final context in which reference-collapsing takes place is the use ofdecltype. As is the case with templates andauto, decltype performs type deduction on expressions that yield types that are eitherT orT&, and decltype then applies C++11’s reference-collapsing rules.  Alas, the type-deduction rules employed bydecltype are not the same as those used during template orauto type deduction.  The details are too arcane for coverage here (theFurther Information section provides pointers to, er, further information), buta noteworthy difference is thatdecltype, given a named variableof non-reference type,deduces the type T (i.e., a non-reference type), whileunder the same conditions, templates andauto deduce the type T&.  Another important difference is thatdecltype’s type deduction depends only on thedecltype expression; the type of the initializing expression (if any) is ignored. Ergo:

  1. Widget w1, w2;
  2.  
  3. auto&& v1 = w1; // v1 is an auto-based universal reference being
  4. // initialized with an lvalue, so v1 becomes an
  5. // lvalue reference referring to w1.
  6.  
  7. decltype(w1)&& v2 = w2; // v2 is a decltype-based universal reference, and
  8. // decltype(w1) is Widget, so v2 becomes an rvalue reference.
  9. // w2 is an lvalue, and it’s not legal to initialize an
  10. // rvalue reference with an lvalue, so
  11. // this code does not compile.

Summary

In a type declaration, “&&” indicates either an rvalue reference or auniversal reference – a reference that may resolve to either an lvalue reference or an rvalue reference. Universal references always have the formT&& for some deduced type T.

Reference collapsing is the mechanism that leads to universal references (which are really just rvalue references in situations where reference-collapsing takes place) sometimes resolving to lvalue references and sometimes to rvalue references. It occurs in specified contexts where references to references may arise during compilation. Those contexts are template type deduction,auto type deduction,typedef formation and use, and decltype expressions.

Acknowledgments

Draft versions of this article were reviewed by Cassio Neri, Michal Mocny, Howard Hinnant, Andrei Alexandrescu, Stephan T. Lavavej, Roger Orr, Chris Oldwood, Jonathan Wakely, and Anthony Williams.  Their comments contributed to substantial improvements in the content of the article as well as in its presentation.

Notes

[1] I discuss rvalues and their counterpart, lvalues, later in this article. The restriction on lvalue references binding to rvalues is that such binding is permitted only when the lvalue reference is declared as a reference-to-const, i.e., a const T&.

[2] I’m ignoring the possibility of bounds violations. They yield undefined behavior.

[3] std::vector::push_back is overloaded. The version shown is the only one that interests us in this article.

Further Information

C++11, Wikipedia.

Overview of the New C++ (C++11), Scott Meyers, Artima Press, last updated January 2012.

C++ Rvalue References Explained, Thomas Becker, last updated September 2011.

decltype, Wikipedia.

“A Note About decltype,” Andrew Koenig, Dr. Dobb’s, 27 July 2011.

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 内六角螺钉滑了怎么办 三视图看不出来怎么办 小猫断奶以后母猫涨奶怎么办 手机螺丝滑丝了怎么办 螺丝孔道滑丝了怎么办 螺丝生锈了拧不下来怎么办 钣金加工六角网孔变形怎么办 外六角螺帽滑丝怎么办 内六角螺丝螺帽滑丝怎么办 一字螺丝钉脱扣拧不下来怎么办 一字螺丝拧花了怎么办 小螺丝卸不下来怎么办 机油螺丝滑丝了怎么办 刚滑双板膝盖滑的疼怎么办 lv包真皮弄脏了怎么办 lv包压变形了怎么办 lv的包包被压了怎么办 固态硬盘太小了怎么办 联想笔记本网络连接不可用怎么办 联想g50玩dnf卡怎么办 手机有wifi电脑没有网怎么办 电脑网卡被禁用了怎么办 win8系统装win7蓝屏怎么办 联想笔记本装win7蓝屏怎么办 联想g40-70开机黑屏怎么办 新主机开不了机怎么办 联想720s笔记本闪屏怎么办 华硕k40ie显卡坏了怎么办 开机黑屏进入bois后怎么办 2根内存条不兼容怎么办 联想笔记本r720系统崩溃怎么办 联想天逸310卡怎么办 新买的鼠标没反应怎么办 联想笔记本触屏鼠标失灵怎么办 无线鼠标接收器丢了怎么办 联想笔记本系统重装失败怎么办 联想笔记本屏幕闪屏怎么办 种植牙螺钉掉了怎么办 水管牙断里面了怎么办 水龙头起泡器不起泡怎么办 14mm乘8mm残留怎么办