Home > Articles > Programming > C/C++

C++ Reference Guide

Hosted by

Toggle Open Guide Table of ContentsGuide Contents

Close Table of ContentsGuide Contents

Close Table of Contents

The rvalue Reference Proposal, Part I

Last updated Jan 1, 2003.

Without exaggeration, the recent proposal to add a new type of references called rvalue references is the most significant core language change since the ratification of ISO C++ in 1998. In the first part of this series I introduce rvalue references and show their usefulness in the implementation of move semantics.

Rvalue References

Document N1690 proposes that rvalue references, a new type of references, be added to C++. Rvalue reference binds to rvalues even if not const qualified. Rvalue references enable programmers to:

  • Eliminate unnecessary expensive copies of objects, thereby facilitating the implementation of move semantics.
  • Solve major usability problems with generic forwarding utilities, enabling users to implement what is known as perfect forwarding.
  • Solve usability problems in components where binding an rvalue to a non-const reference is not a logical error.

Before discussing move semantics, let’s look at the syntax of the new reference type. An rvalue reference to C is declared as C&& . To distinguish an rvalue reference from the existing reference C&, the latter is now termed an lvalue reference.

rvalue references behave just like the existing lvalue references except that they can bind to an rvalue:

C c;
C& rc=c; //ordindary lvalue reference
C&& rrc=C(); //OK, bind a temporary to an rvalue reference
C& illegal = C(); //compilation error

Overloading and Overload Resolution

An rvalue reference and an lvalue reference are distinct types. Therefore, you may declare overloaded versions of the same function that differ in their reference arguments:

void f(const A& a); // #1 lvalue reference
void f(A&& a);    // #2 rvalue reference

The overload resolution rules are:

  • Rvalues will prefer rvalue references. lvalues will prefer lvalue references.
  • CV qualification conversions are secondary to rvalue/lvalue conversions.
  • rvalues can still bind to a const lvalue reference (e.g., const A&), unless there is a more attractive rvalue reference in the overload set.
  • lvalues can bind to an rvalue reference, but will prefer an lvalue reference if available.
  • The rule that a more cv-qualified object can not bind to a less cv-qualified reference stands both for lvalue and rvalue references.

Examples:

struct A {};
A f();
const A cf();
A a;
const A ca;
f(a);        // calls #1
f(ca);       // calls #1
f(source());    // calls #2
f(const_source()); // calls #1

The first f() call prefers the lvalue reference (#1) because the argument a is an lvalue. lvalue to rvalue conversion is a poorer match than A& -> const A& conversion. The second f() call is an exact match for #1. The third f() call is an exact match for #2. The fourth f() call can’t bind to #2 because const A&& to A&& conversion isn’t permitted. It calls #1 via an rvalue to lvalue conversion.

Move Semantics

The primary reason for adding rvalue references is move semantics. Unlike the well-known concept of copying, moving means that a target object pilfers the resources of the source object, instead of reduplicating those resources or sharing them "Why would anyone want that?" you’re probably asking. In most cases, you really want the ordinary copy semantics. However, in some cases making a copy of an object is both expensive and unnecessary. C++ already implements move semantics in several places:

In the case of auto_ptr assigning or copy constructing (move-constructing, actually) an auto_ptr object pilfers the source object, transferring its resources to the target object. After the execution of the following code snippet:

auto_ptr <int> a(new int);
auto_ptr<int> b (a); //transfer resource ownership from a to b

b has become the new owner of the resource that a acquired during its initialization, whereas a has relinquished its ownership of that resource.

Moving versus Copying

To assess the potential performance gains of move semantics, let’s look at string swapping. A naïve implementation would look like this:

void swapstr(string &a, string & b)
{
 string temp = a;
 a=b;
 b=temp;
}

Copying a string object entails the allocation of raw memory, subsequent copying the characters from the source string to the target etc. By contrast, an implementation that moves strings is more efficient:

void moveswapstr(string &a, string & b)
{
//pseudo code, but you get the idea
 size_t sz=a.size();
 const char *p= a.data();
//move b’s resources to a
 a.setsize(b.size()); 
 a.setdata(b.data());
//move a’s former resources to b
 b.setsize(sz); 
 b.setdata(p);
}

Instead of allocating raw memory dynamically, copying characters from the source to the target object and setting the size value, a move operation merely swaps the memory addresses and the sizes of the two strings.

Fortunately, specialized versions of swap() in the Standard Library are already implemented according to this idiom so you don’t have to rewrite it. However, the point is clear: moving is can be dramatically faster than copying.

Rvalue references facilitate the implementation of move semantics. For example, it enables implementers to add new overloaded versions of an assignment operator and move-constructor that are more efficient than the traditional copy assignment operator and copy constructor:

class string
{
public:
  // copy semantics
  string(const string& s);
  string& operator=(const string& s);
 
  // move semantics
  string(string&& s);
  string& operator=(string&& s);
  // ...
};

The move overloaded versions are automatically called when the argument is an rvalue. Similarly, a function may return an rvalue reference. A call to such a function is an rvalue:

int & f()
{
return *new int;
}
f()=5;
int&& g()
{
return 6;
}
int n=g(); //OK
g()=n; //error, lvalue expected

In the second part I will discuss forwarding and show how rvalue references enable the implementation of perfect forwarding.