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 II

Last updated Jan 1, 2003.

This part discusses the rules of rvalue references and explains how rvalue references solve the argument forwarding problem.

The Forwarding Problem

Generic function adaptors such as std::bind1st, std::bind, and std::tr1::function transform one function signature into another by binding some of the parameters. Ideally, the parameters that were left unbound in the transformed function should behave identically to matching parameters of the original function. For example, a const lvalue reference:

const T& 

should bind to both lvalue and rvalue arguments, whereas a non-const lvalue reference:

T&

should bind to a non-const lvalue, but refuse to bind to rvalues and const lvalues:

template <class T> void g(T& t);
int n=1;
int&rn=n;
const int& crn=n;
g(rn);//OK, lvalue 
g(5); // compilation error, can’t bind T& to rvalue
g(crn); //compilation error, can’t modify a const object

The common solution to this problem is const-based overloading, whereby each parameter gets two overloads:

template <class T> void g(T& t); #1
template <class T> void g(const T& t); #2
g(rn);// calls #1
g(5); // ok, calls #2 
g(crn); //ok, calls #2

However, as the number of parameter grows, the number of overloads required increases exponentially. If you have two parameters, you will need four overloads of the same binder. For three parameters you’ll need 2^3 overloads.

Rvalue references enable you to implement what is known as perfect forwarding without resorting to const-based overloading. The proposed rules say that if t is a non-const lvalue reference, then the reference collapsing rules (explained below) ensure that T&& is a non-const lvalue reference, and thus will not bind to rvalues. If t is not a reference (i.e. pass-by-value), then the parameter T&& will bind to rvalues:

template <class T> void h(A & a1, const A& a2)
template <class T> void forward(T&& t1, T&& t2) {h(t1, t2); }
int n=0;
forward(1,10); //compilation error
forward (n, 10); //OK
In the first forward() call, the first argument is an rvalue that is bound to t1. When t1 is passed to h(), a compilation error occurs because it’s illegal to bind a non-const lvalue A& to an rvalue. In contemporary C++, you’d have to define two overloads of h() for this to work:
void (A& t,...);
void (const A& t,...);
In the second forward() call, the argument n is a non-cost lvalue. It binds to t1 while retaining its non-const lvalue identity. When t1 is forwarded to h(), it binds to a1, which is also a non-const lvalue. The second argument 10 is an rvalue. It can only bind to const A&.

In conclusion, the use of rvalue references enables you to implement perfect forwarding with a single version of forward().

Reference Collapsing Rules

Reference collapsing rules define what happens when a reference to references is formed. In C++98, there is only one reference collapsing rule: T& & or a reference to a reference, collapses to T&:

void g(int & ri) {++ri;} // int& & -> int&
void f(int & ri) {g(ri);} 
The rvalue proposal extends the reference collapsing rules to include rvalue references as well:
  • A& & -> A& //as in C++98
  • A& && -> A&
  • A&& & -> A&
  • A&& && -> A&&

According to these rules, a forwarding function can replicate both the cv qualification and l/rvalueness of its argument so that the forwarded-to function sees precisely the same argument.

Rvalue Cast

The rvalue reference proposal introduces new versions of the C++ cast operators which convert an operand to an rvalue. For example, the static_cast<T&&>(t) expression returns an lvalue if t is an lvalue type. Otherwise, the result is an rvalue:

template <class T>
T&& move(T&& x)
{
  return static_cast<T&&>(x); //converts x to rvalue
}

Similar changes are proposed for the rest of the cast operators.

Move Semantics and RVO

In part I, I mentioned the Return Value Optimization (RVO). In essence, the RVO elides unnecessary copies in an assignment expression that calls a function which returns a local automatic object by value:

A f()
{
 return A(); 
}

A b=f();
A naïve C++ implementation would perform the following steps to initialize b:
  • create a temporary A inside f()
  • copy construct that temporary on the caller’s stack
  • destroy the temporary created inside f()
  • copy-construct b from the said copy
  • destroy the said copy

Modern C++ compilers optimize away these steps by rewriting f() as void f(A&). The result is constructed directly into the lvalue argument. Thus, instead of creating copies, the implementation modifies the state of a given object directly.

The rvalue proposal addresses this optimization. The authors propose that a function returning a non-cv-qualified object with automatic storage should cast the result implicitly to rvalue:

string
operator+(const string& x, const string& y)
{
 string result;
 result.reserve(x.size() + y.size());
 result = x;
 result += y;
 return result; // as if return static_cast<string&&>(result);
}

The rationale behind this implicit cast is an automatic hierarchy of move semantics:

  • Elide the move/copy, if possible
  • Else if there is a move constructor, use it
  • Else if there is a copy constructor, use it
  • Else the program is ill formed

Summary

The rvalue references proposal is directly related to other proposals for supporting move semantics and solutions to the argument forwarding problem. At this stage, the proposal is still being reviewed and is subjected to alterations.

Speaking of alterations, I’m not particularly keen on the T&& notation for two reasons: it could mislead readers into thinking that T is a reference to a reference (who knows, C++21xx might even need this beast!), just as T** denotes a pointer to a pointer. Additionally, T&& requires more keystrokes, which can be quite painful when dealing with long parameter lists. T! or T% seem better options, requiring fewer keystrokes while eliminating the confusion with lvalue references.