Rvalue reference pitfalls

I just finished adding initial C++11 support to ODB (will write more about that in a separate post) and, besides other things, this involved using rvalue references, primarily to implement move constructors and assignment operators. While I talked about some of the tricky aspects of rvalue references in the “Rvalue references: the basics” post back in 2008 (it is also a good general introduction to the subject), the experience of writing real-world code that uses this feature brought a whole new realization of the potential pitfalls.

Probably the most important thing to always keep in mind when working with rvalue references is this: once an rvalue reference is given a name, it becomes an lvalue reference. Consider this function:

void f (int&& x)
{
}

What is the type of the argument in this function? It is an rvalue reference to int (i.e., int&&). And what is the type of the x variable in f()’s body? It is a normal (lvalue) reference to int (i.e., int&). This takes some getting used to.

Let’s see what happens when we forget about this rule. Here is a naive implementation of a move constructor in a simple class:

struct base
{
  base (const base&);
  base (base&&);
};
 
struct object: base
{
  object (object&& obj)
      : base (obj),
        nums (obj.nums)
  {
  }
 
private:
  std::vector<int> nums;
};

Here, instead of calling move constructors for base and nums we call their copy constructors! Why? Because obj is an lvalue reference, not an rvalue one. The really bad part to this story is this: if you make such a mistake, there will be no compilation or runtime error. It will only manifest itself as sub-optimal performance which can easily go unnoticed for a long time.

So how do we fix this? The fix is to always remember to convert the lvalue reference back to rvalue with the help of std::move():

  object (object&& obj)
      : base (std::move (obj)),
        nums (std::move (obj.nums))
  {
  }

What if one of the members doesn’t provide a move constructor? In this case the copy constructor will silently be called instead. This can also be sub-optimal or even plain wrong, for example, in case of a raw pointer. If the member’s type provides swap(), then this can be a good backup plan:

  object (object&& obj)
      : base (std::move (obj))
  {
    nums.swap (obj.nums);
  }

Ok, that was a warmup. Ready for some heavy lifting? Let’s start with this simple code fragment:

typedef int& rint;
typedef rint& rrint;

What is rrint? Right, it is still a reference to int. The same logic holds for rvalue references:

typedef int&& rint;
typedef rint&& rrint;

Here rrint is still an rvalue reference to int. Things become more interesting when we try to mix rvalue and lvalue references:

typedef int&& rint;
typedef rint& rrint;

What is rrint? Is it an rvalue, lvalue, or some other kind of reference (lrvalue reference, anyone)? The correct answer is it’s an lvalue reference to int. The general rule is that as soon as we have an lvalue reference anywhere in the mix, the resulting type will always be an lvalue reference.

You may be wondering why on earth would anyone create an lvalue reference to rvalue reference or an rvalue reference to lvalue reference. While you probably won’t do it directly, this can happen more often than one would think in template code. And I don’t think the resulting interactions with other C++ mechanisms, such as automatic template argument deductions, are well understood yet.

Here is a concrete example from my work on C++11 support in ODB. But first a bit of context. For standard smart pointers, such as std::shared_ptr, ODB provides lazy versions, such as odb::lazy_shared_ptr. In a nutshell, when an object that contains lazy pointers to other objects is loaded from the database, these other objects are not loaded right away (which would be the case for normal, eager pointers). Instead, just the object ids are loaded and the objects themselves can be loaded later, when and if required.

A lazy pointer can be initialized with an actual pointer to a persistent object, in which case the pointer is said to be loaded. Or we can initialize it with an object id, in which case the pointer is unloaded. Here are the signatures of the two constructors in question:

template <class T>
class lazy_shared_ptr
{
  ...
 
  template <class ID>
  lazy_shared_ptr (database&, const ID&);
 
  template <class T1>
  lazy_shared_ptr (database&, const std::shared_ptr<T1>&);
};

Seeing that we now have rvalue references, I’ve decided to go ahead and add move versions for these two constructors. Here is my first attempt:

  template <class ID>
  lazy_shared_ptr (database&, const ID&);
 
  template <class ID>
  lazy_shared_ptr (database&, ID&&);
 
  template <class T1>
  lazy_shared_ptr (database&, const std::shared_ptr<T1>&);
 
  template <class T1>
  lazy_shared_ptr (database&, std::shared_ptr<T1>&&);

Let’s now see what happens when we try to create a loaded lazy pointer:

shared_ptr<object> p (db.load<object> (...));
lazy_shared_ptr<object> lp (db, p);

One would expect that the third constructor will be used in this fragment but that’s not what happens. Let’s see how the overload resolution and template argument deduction work here. The type of the second argument in the lazy_shared_ptr constructor call is shared_ptr<object>& and here are the signatures that we get:

lazy_shared_ptr (database&, const shared_ptr<object>&);
lazy_shared_ptr (database&, (shared_ptr<object>&)&&);
lazy_shared_ptr (database&, const shared_ptr<object>&);
lazy_shared_ptr (database&, shared_ptr<object>&&);

Take a closer look at the second signature. Here the template argument is an lvalue reference. On top of that we add an rvalue reference. But, as we now know, this is still just an lvalue reference. So in effect our candidate list is as follows and, unlike our expectations, the second constructor is selected:

lazy_shared_ptr (database&, const shared_ptr<object>&);
lazy_shared_ptr (database&, shared_ptr<object>&);
lazy_shared_ptr (database&, const shared_ptr<object>&);
lazy_shared_ptr (database&, shared_ptr<object>&&);

In other words, the second constructor, which was supposed to take an rvalue reference was transformed to a constructor that takes an lvalue reference. This is some profound stuff. Just think about it: given its likely implementation, this constructor can now silently “gut” an lvalue without us ever indicating this desire with an explicit std::move() call.

So how can we fix this? My next attempt was to strip the lvalue reference from the template argument, just like std::move() does for its return value:

  template <class ID>
  lazy_shared_ptr (
    database&,
    typename std::remove_reference<ID>::type&&);

But this inhibits template argument deduction and there is no way (nor desire, in my case) to specify template arguments explicitly for constructor templates. So, in effect, the above constructor becomes uncallable.

So what did I do in the end? Nothing. There doesn’t seem to be a way to provide such a move constructor. The more general conclusion seems to be this: function templates with arguments of rvalue references that are formed directly from template arguments can be transformed, with unpredictable results, to functions that have lvalue references instead. Preventing this using std::remove_reference will inhibit template argument deduction.

Update: I have written a follow up to this post that discusses some of the suggestions left in the comments as well as presents a solution for the above problem.

11 Responses to “Rvalue reference pitfalls”

  1. Achilleas Margaritis Says:

    Good post. I’ve argued even with the Committee members that rvalue references is the wrong way to go on improving performance of functions that return complex data structures. I’ve indicated various issues, and now we see yet another one.

    What I’d prefer, instead of rvalue references, is temporary copy constructors/assignment operators. Just like with the ‘explicit’ keyword, a keyword like ‘temporary’ before the constructor would indicate that the given object will be destroyed just after the constructor call, enable optimization of constructors and assignment operators, without interference from the type system.

    That’s, of course, an almost random thought on a thorny problem; more thought would certainly be required.

  2. Boris Kolpackov Says:

    At the moment I still think rvalue references are a good thing. They are complex, yes, but I don’t think this complexity is easily avoidable. At least, I trust that people working on the committee wouldn’t have missed a better way if there was one.

  3. Thomas Says:

    “There doesn’t seem to be a way to provide such a move constructor. The more general conclusion seems to be this: function templates with arguments of rvalue references that are formed directly from template arguments can be transformed, with unpredictable results, to functions that have lvalue references instead.

    Hello. You seem to have miss the part about perfect forwarding when studying rvalue reference. The deduction rules are different when using rvalue reference and template. They are designed to allowed perfect forwading. Try this :

    void inner(const int& i)
    {
    std::cout
    void outer(T&& t)
    {
    inner(std::forward(t);
    }

    int i;
    outer(i); // will go to inner(const int&)
    outer(5); // will go to inner(int&&)

    So in your case, you need only 3 constructors to lazy_shared_ptr. It should look like this :

    // template case, perfect forward lvalue or rvalue all in one
    template
    lazy_shared_ptr (database& d, ID&& id)
    {
    d.init(std::forward(id));
    }

    // non template case, constructor for lvalue
    template
    lazy_shared_ptr (database& d, const std::shared_ptr& t)
    {
    d.init(t);
    }

    // non template case, constructor for rvalue
    template
    lazy_shared_ptr (database& d, std::shared_ptr&& t);
    {
    d.init(std::move(t));
    }

  4. Thomas Says:

    Sorry, it should be :
    inner(std::forward “Angle brackets” T “Angle brackets”(t);

  5. Benjamin Schindler Says:

    Humm… the reason why a named r-value reference is an l-value because it can be used multiple times. So, in your example:

    object (object&& obj)
    : base (std::move (obj)),
    nums (std::move (obj.nums))
    {
    }

    you use obj 2 times. Since the base constructor is called first, obj.nums can be set to an invalid value. Why? If the base constructor constructs an object using a move constructor, obj.nums could be invalidated leading to (I think) undefined behavior..
    Am I completely off on this?

  6. Boris Kolpackov Says:

    Benjamin, the base constructor will only move the ‘base’ part of the object. The nums members, which is in the ‘derived’ part, will be unaffected (base doesn’t know anything about it).

  7. Boris Kolpackov Says:

    Thomas, no I haven’t missed the part about perfect forwarding and I realize that the same mechanism that caused problems in my case is what allows perfect forwarding in the first place. I should have probably mentioned that, but now you have corrected my omission, thanks ;-).

    Your suggestion about the constructor signatures, however, won’t works. There are several problems with. Firstly, this set of constructors won’t allow initialization with a const reference to id:

    const string id (”abc”);
    lazy_shared_ptr<object> lp (db, id);

    Then, there is the same problem when initializing with a non-const lvalue reference to shared_ptr. Instead of the second constructor, the first constructor will be called:

    shared_ptr<object> p (…);
    lazy_shared_ptr<object> lp (db, p);

  8. Jonathan Rogers Says:

    One thing I would like to point out is that your use of two constructors, one taking a const std::shared_ptr& and the other taking an std::shared_ptr&& might not be the best approach.

    Because of the fact that you you have the && version, it implies that you are planning on storing the shared ptr in your object.

    The correct thing to do in this case is to have just one constructor, a constructor that takes the shared_ptr by value. You then treat the by value argument as you do in the in the shared_ptr&& version, using std::move to take the pointer.

    This will handle all cases efficiently while only requiring one constructor.

    In general, if you find yourself writing two versions of a function, one taking an Rvalue reference, you can probably use this optimisation. (Except for the actual move constructor of an object)

  9. Boris Kolpackov Says:

    Jonathan, very interesting idea. I believe this will work, provided the argument type supports move construction. The cost is just an extra move constructor call, which should be cheap (or even inlined away).

  10. Johannes Says:

    This article confuses the type of a variable “x” with the type of an expression that refers to it (”x”). The type of the variable in your example is and stays “int&&”. The expression that refers to it has type “int” . That type is neither an rvalue reference nor lvalue reference. But what is important is that that expression is an lvalue. Instead of an xvalue. An expression has never a reference type in C++.

  11. haohaolee Says:

    FYI, this article discussed the similar problem
    http://cpptruths.blogspot.com/2012/03/rvalue-references-in-constructor-when.html