Archive for November, 2008

Rvalue-references: the basics

Monday, November 24th, 2008

One of the language changes in the next C++ standard is the addition of rvalue-references. While some people believe this mechanism will be used by a small number of library writers, I think that it will have a wider impact on the way we write software in C++. In either case, however, it will be beneficial to understand the nuances of rvalue-references even if you don’t use them in your code directly. Today I will cover the motivation and basics of rvalue-references, including some counter-intuitive aspects towards the end. The next installment will deal with the changes that rvalue-references will bring to C++ programs.

While the next C++ standard still has some way to go before being published, the GCC team has implemented support for some parts of the standard in g++ 4.3 and 4.4 (see C++0x Language Support in GCC for details). All the sample code presented below can be tried with g++ 4.3.2 in the C++0x mode (the -std=c++0x option).

Every expression in C++ evaluates to either lvalue or rvalue. The standard has a page-long list of rules that define these two terms. It is more intuitive (but less precise) to think of an lvalue as referring to an object that has a name somewhere in the program. This name can be a variable name or a pointer. An rvalue refers to an object that does not have a name. Here are some examples:

struct S {};
S s;
 
s    // lvalue
S () // rvalue
 
S  f ();
S& g ();
S* h ();
 
f ()  // rvalue
g ()  // lvalue
*h () // lvalue

The last two expressions are lvalues because one needs a named object to bind a reference to it or take its address.

Because an rvalue object is always allocated on the stack and is unnamed, its life is short (unless bound to a const reference variable). As a result, modifying an rvalue does not make much sense and usually means a programming error. To prevent such errors C++ does not allow binding of non-const references to rvalues:

void f (S&);
void g (const S&);
 
f (S ()); // error
g (S ()); // ok

An issue with the above rules arises when we want to make a copy of an rvalue and this operation is expensive. Because the rvalue object will be gone immediately after we make the copy, “transplanting” the internals of an rvalue to our copy seems like a natural optimization. Consider the following example:

struct IntVector
{
  IntVector (const IntVector& x)
  {
    v_ = new int[x.size_];
    size_ = x.size_;
    memcpy (v_, x.v_, size_ * sizeof (int));
  }
  ...
private:
  int* v_;
  size_t size_;
};
 
IntVector load ()
{
  IntVector r;
  // Load the data into r.
  return r;
}
 
IntVector v (load ());

While some compilers may optimize the call to the copy constructor away (RVO/NRVO optimization), this depends on the implementation of load() and is not guaranteed.

What we would like here is to transfer the contents of the returned rvalue into v, something along these lines:

IntVector (IntVector& x)
{
  v_ = x.v_;
  size_ = x.size_;
  x.v_ = 0;
  x.size_ = 0;
}

This approach does not work for two reasons: First, because we modify the source object, we have to pass it as a non-const reference. But an rvalue can only be bound to a const reference. Second, our new version of the copy constructor will just as happily transplant the internals of lvalues:

IntVector v1;
IntVector v2 (v1); // v1 is no longer usable

What we need is a special kind of reference that will bind to rvalues and allow their modification. And that’s exactly what the next version of the C++ standard adds: rvalue-references. Unfortunately, the interaction of rvalue-references with lvalue-references (that’s what the plain old references are now called) and the rest of the language is quite complex. It is also fairly easy to shoot yourself in the foot, even as just the user of rvalue-optimized types. What follows is the description of some of the rules governing rvalue-references.

An rvalue-reference of type T is declared as T&&. The overload resolution between rvalue- and lvalue-references is similar to the way it works for const and non-const lvalue-references. For example:

void f (int&);
void f (int&&);
void g (int&&);
 
int h ();
int i;
 
f (i);     // f (int&) is called
f (h ());  // f (int&&) is called
g (i);     // ok

That is, when passing an lvalue and the lvalue overload is available, it is preferred. Otherwise, the rvalue version is called since an lvalue can be used anywhere an rvalue can. This is a fairly important point which means that in the majority of cases when you provide a constructor or a function taking an rvalue-reference, you will also want to provide the overloaded version taking const an lvalue-reference. Otherwise, lvalues will be treated as rvalues (std::move(), which is discussed shortly, is one example where this behavior is actually desired). Here is how we could overload our IntVector constructors:

IntVector (const IntVector& x)
{
  v_ = new int[x.size_];
  size_ = x.size_;
  memcpy (v_, x.v_, size_ * sizeof (int));
}
 
IntVector (IntVector&& x)
{
  v_ = x.v_;
  size_ = x.size_;
  x.v_ = 0;
  x.size_ = 0;
}

Let’s consider the call to load() once again:

IntVector v (load ());

If the compiler didn’t optimize the copying of the return value, the process works as follows. The r object inside the function is first copied into a temporary and then destroyed. After load() returns, the v object is copy-constructed from the temporary and the temporary is destroyed.

The new constructor now takes care of efficiently copying the data from the temporary returned by load() to the v object. But what about the other half of the process. Namely copying of the r object inside the load() function to the temporary? The r object is an lvalue and, as a result, the expensive copy constructor will be used. One way to solve this would be to explicitly convert r to an rvalue-reference with the help of std::move():

IntVector load ()
{
  IntVector r;
  // Load the data into r.
  return std::move (r);
}

std::move() simply converts any rvalue or lvalue to an rvalue reference:

template <typename T>
typename remove_reference<T>::type&&
move (T&& x)
{
  return x;
}

While this works, it sure is verbose and easy to forget. Plus, in template code we don’t know whether the argument type has a constructor capable of taking an rvalue-reference. The C++ standard fixes this in quite an interesting way. The standard provides criteria when a C++ compiler is allowed to optimize away temporaries and copying that is used to return a value from a function (called elision of copy operations). While compilers can perform this optimization, they are not required to do so. But then the standard adds that when the criteria for elision of copy operations are met but the compiler is not performing such elision and the object to copy is an lvalue, then the overload resolution to select the copy constructor is first performed as if the object were an rvalue. One of the criteria for elision of copy operations is returning an automatic (stack-allocated) object. If you think about it, it makes perfect sense: the object being copied is essentially an rvalue since it will be destroyed once the copying is complete and nobody else can access it between now and then.

All this means that in our implementation of load() when we return the result, the r object is first considered as if it were an rvalue and the fast constructor will be called automatically without us explicitly calling std::move().

Now to the less intuitive side of rvalue-references. Named rvalue-references are treated as lvalues while unnamed rvalue-references are treated as rvalues. In other words, once an rvalue-reference is given a name, it starts acting as an lvalue. The following code snippet helps illustrate this point:

void f (int&);
int&& g ();
 
int&& i = 2;
f (i);       // ok
f (g ());    // error

Without keeping this in mind, it is natural to expect one thing but get something completely different:

void f (const int&);
void f (int&&);
 
void g (int&& x)
{
  f (x); // calls f (const int&)
}

If we wanted to call f(int&&), we would need to first convert x to an unnamed rvalue-reference, for example, with the help of std::move():

void g (int&& x)
{
  f (std::move (x)); // calls f (int&&)
}

Another interesting aspect of rvalue-references is what happens when we try to create an rvalue-reference to lvalue-reference or vice versa. While this cannot be done directly (T&&& is illegal), it is easy to achieve, most likely unintentionally, with typedef or template type arguments. In this case, the resulting type will always be an lvalue-reference:

typedef int&  ilr;
typedef int&& irr;
 
void f (int&)  {}
void f (ilr&&) {} // error: same as void f (int&)
void f (irr&)  {} // error: same as void f (int&)
 
template <typename T>
void g (T&);
 
template <typename T>
void h (T&&);
 
g<int&&> (..); // void g (int&)
h<int&> (..);  // void h (int&)

Next time: how rvalue-references will affect the way we write software in C++.

delete, operator delete() and NULL pointers

Monday, November 17th, 2008

In C++ both delete or, more precisely, the delete expression and operator delete() can be called with a NULL pointer. The standard requires that such a call shall have no effect. There is, however, a subtle difference in how these two cases are handled under the hood which can be useful to know, especially when writing performance-critical applications.

When we deallocate a memory block using the delete expression, the compiler inserts a check for NULL before calling the destructor and operator delete(). Consider the following code fragment as an example:

buffer::~buffer ()
{
  delete[] buf_;
}

During compilation this code snippet is transformed to something along these lines (there is no destructor call because, presumably, buf_ is declared as char*):

buffer::~buffer ()
{
  if (buf_)
    operator delete[] (buf_);
}

Consider now an alternative implementation of the buffer destructor (here we assume that the constructor was also changed to use operator new(n) instead of new char[n]):

buffer::~buffer ()
{
  operator delete (buf_);
}

In this case nothing extra is inserted by the compiler and the check for NULL is performed by the implementation of operator delete(). Here is the default implementation of this operator provided by g++:

void
operator delete (void* ptr) throw ()
{
  if (ptr)
    std::free (ptr);
}

As you can see, both alternatives ignore NULL pointers. However, the delete expression checks the pointer before calling the deallocation operator while with operator delete(), the function call is made unconditionally and the pointer is checked inside the implementation. If such a call is on a critical path and the pointer is frequently NULL (e.g., a buffer is left empty), then checking for NULL before calling operator delete() can make a difference.

Why develop new products during a recession

Monday, November 10th, 2008

For the past year and a half I’ve been driving past a new BMW dealership as it is being built. The project started just before the beginning of the sub-prime saga when the economy was still good, credit was easy, and people were lining up to buy new cars. Now the new building is almost ready, the economy is in a bad shape, and dealerships are struggling to stay afloat.

A number of prominent VCs published letters they sent to their companies on how to survive the downturn. The standard advice includes not hiring, shutting down or cutting R&D, and making everyone, including receptionists, sell. This approach, which boils down to getting as much cash in and as little out, sounds logical, especially for a startup strapped for cash. But what if a company has cash reserves sufficient to last several years even if the sales dried up completely? Is there a better strategy than hibernating until the economic sprint comes back?

A company that operates in the survival mode during downturns ramps up new product development during boom times. In a good economy financing is easy and, as a result, many new companies are being started. There is an increasing demand for engineers and there is a lot of noise from all the new products being introduced into the market.

During a downturn, such a company concentrates on sales which are harder and harder to get (unless the company is selling something that is in demand during a recession). Sales people, at least the good ones, would be let go as the last resort. Since the majority of companies tend to operate in the survival mode, there is not much opportunity to improve the quality of the sales team, at least not until later when companies start running out of cash. This company got an uphill battle in both good and bad economic conditions.

Let’s now examine how the contrarian approach works, assuming the company has enough cash to survive several years with significantly reduced sales. Such a company would ramp up new product development during the downturn and slash down the sales effort, perhaps even purging the sales team. At this time it should be easier and cheaper to pick up quality engineers since there are more of them on the market and there is less competition from other companies. It is also easier to introduce new products and appear as a market leader during a recession.

Towards the end of the downturn the company can try to improve the quality of its sales team by hiring people from failed companies. As boom times come back, the contrarian approach yields new products ready for the market and the sales team ready for the renewed interest. At this point the company becomes cautious of any aggressive expansions as costs increase. Instead, it concentrates on accumulating enough cash to repeat the cycle when the economy turns bad again.

During boom times companies rush to get to market as quickly as possible in order not to miss opportunities. A downturn, therefore, could be a perfect time to develop and introduce radical and unproven new technology that can take years to get right.

The contrarian approach is logical for a bootstrapped or established business that got a chance to accumulate substantial cash reserves during a boom. It is the way investing, especially the VC type, works that forces companies into the survival mode. Raising venture capital in a good economy is a lot easier than during a downturn. It is also easier to get investment for an idea that is in a “hot” market such as e-commerce during the dot-com boom and social networks more recently. VCs also expect their companies to expand rapidly. This makes a VC-funded company burn cash by rapidly growing in a crowded and noisy field with expensive and scarce engineers.

There are other advantages of expanding during a recession. Office space becomes cheaper as the demand slows. It is easier to negotiate better deals with suppliers and partners as they become dependent on the revenue your business brings. Tax incentives for R&D, starting new businesses, and hiring people are often introduced during recessions to revive the economy. It is also well known that teams become more focused and work harder in the face of a powerful enemy. Recession can be such an enemy. Boom times have the excitement of the overall activity in the field as well as easy sales. But excitement is fleeting while the resolve to outlast a recession stays.

The contrarian approach is not without risks, the biggest of which is running out of cash before the downturn ends. The other problem is finding early customers to use your product and provide feedback. However, one can offer the initial version for free or at a significant discount which may work rather well during a recession when customers presumably need your product but simply cannot afford to pay the full price at the moment.

The survival approach is not without risks either. The biggest of which is expanding into a recession, as the BMW dealership example above illustrates. As with the contrarian approach, there is also the possibility of failing to preserve enough cash and generate enough sales to weather a recession.