You might have heard of the latest invention in C++0x–rvalue references (see for instance, A Brief Introduction to Rvalue References). Depending on your point of view, rvalue references are the best thing since STL, or yet another kludge invented to fix accumulated language design errors in C++. This distinction is pretty important when you are trying to design a new language. Should the D language have rvalue references, or is there a better way? In order to figure that out, one has to understand what problem they solve and what previous language design decisions led to their emergence.

I’ll start with some pretty obvious things, so bear with me. A new perspective on old things often leads to better generalizations.

Lvalues vs rvalues

There are two ways of passing arguments to functions–by value or by reference. In C, passing by reference meant passing a pointer to data. If a function took a pointer, the client had to call it with a pointer–for instance, by taking the address of some data.

Not every piece of data has an address. The ones that do are called lvalues, the ones that don’t are called rvalues. Data has to sit in a named memory location in order to have an address. If it sits in a register, it’s not addressable in the usual sense (what’s a pointer to EAX?). Any data that, even theoretically, could be stored in registers without having a backup copy in main memory, is considered an rvalue. Examples of rvalues are data returned from functions (by value), or immediate results of arithmetic expressions.

In C’s pointer notation, it’s pretty obvious when you can and can’t take the address of data. Consider these examples:

void f(int * pi);
int g();

f(&g()); // error!
f(&(1 + 1)); // error!

Notice that small modifications will make this code compile:

int tmp1 = g();
f(&tmp1);
int tmp2 = 1 + 1;
f(&tmp2);

Why can’t the compiler create temporary variables for us and forgo error messages?

Not a good idea! The fact that f takes a pointer to integer rather than an integer usually means that it wants to modify the integer and make the modification visible to the caller. For instance:

void f(int * pi) { ++*pi; }

Calling such a function with the address of an ephemeral temporary variable that’s not addressable by the caller is usually a bug. We don’t want compilers to bury our bugs. Taking the address of an rvalue is therefore an error both in C and C++.

References

C++ made things a little more complicated by introducing references. When you call a function that takes a reference, you no longer have to explicitly take the address of data you’re passing to it. The compiler will do it implicitly. So the previous example no longer looks so wrong when function f is declared to take a reference:

void f(int & i) { ++i; }
int g();
// caller's code
f(g());
f(1 + 1);

These calls are still flagged as errors–you can’t take a reference to an rvalue–but you can’t figure it out just by examining the caller’s code.

However, passing by reference has other unrelated uses. For instance, passing large data structures by reference may be more efficient than passing them by value. In that case we don’t really care if the original data is an lvalue or an rvalue. We’d like such calls to compile.

Const references

How can a C++ programmer have a cake and eat it too? How can he or she tell the compiler that the argument is passed by reference not because it is to be modified but for the sake of performance, and it’s okay to call it with rvalues?

That should be easy: Since the callee has no interest in modifying the original data, he or she should mark the reference argument as const. The compiler will then allow binding of an rvalue to a const reference.

Indeed, the following code compiles without errors:

void f(const int & i);
int g();

f(g());
f(1 + 1);

It all made sense at the time, until the auto_ptr entered the picture.

Next time: The ato_ptr disaster.

About these ads